diff --git a/auth/auth-module.nix b/auth/auth-module.nix index fa831b5..22a502f 100644 --- a/auth/auth-module.nix +++ b/auth/auth-module.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let inherit (lib) mkOption @@ -7,12 +12,12 @@ let auth-passthru = config.selfprivacy.passthru.auth; keys-path = auth-passthru.keys-path; # generate OAuth2 client secret - mkKanidmExecStartPreScript = oauthClientID: linuxGroup: + mkKanidmExecStartPreScript = + oauthClientID: linuxGroup: let secretFP = auth-passthru.mkOAuth2ClientSecretFP linuxGroup; in - pkgs.writeShellScript - "${oauthClientID}-kanidm-ExecStartPre-script.sh" '' + pkgs.writeShellScript "${oauthClientID}-kanidm-ExecStartPre-script.sh" '' set -o pipefail set -o errexit if ! [ -f "${secretFP}" ] @@ -22,17 +27,16 @@ let chmod 640 "${secretFP}" fi ''; - mkKanidmExecStartPostScript = oauthClientID: linuxGroup: isMailserver: + mkKanidmExecStartPostScript = + oauthClientID: linuxGroup: isMailserver: let kanidmServiceAccountName = "sp.${oauthClientID}.service-account"; kanidmServiceAccountTokenName = "${oauthClientID}-service-account-token"; - kanidmServiceAccountTokenFP = - auth-passthru.mkServiceAccountTokenFP linuxGroup; + kanidmServiceAccountTokenFP = auth-passthru.mkServiceAccountTokenFP linuxGroup; isRW = oauthClientID == "selfprivacy-api"; in - pkgs.writeShellScript - "${oauthClientID}-kanidm-ExecStartPost-script.sh" - ('' + pkgs.writeShellScript "${oauthClientID}-kanidm-ExecStartPost-script.sh" ( + '' export HOME=$RUNTIME_DIRECTORY/client_home readonly KANIDM="${pkgs.kanidm}/bin/kanidm" @@ -82,13 +86,12 @@ let + lib.strings.optionalString isRW '' $KANIDM group add-members idm_admins "${kanidmServiceAccountName}" '' - ); + ); in { options.selfprivacy.auth = { clients = mkOption { - description = - "Configurations for OAuth2 & LDAP servers clients services. Corresponding Kanidm provisioning configuration and systemd scripts are generated."; + description = "Configurations for OAuth2 & LDAP servers clients services. Corresponding Kanidm provisioning configuration and systemd scripts are generated."; default = { }; type = types.attrsOf ( types.submodule { @@ -107,69 +110,58 @@ in }; enablePkce = mkOption { type = lib.types.bool; - description = - "Whether PKCE must be used between client and Kanidm."; + description = "Whether PKCE must be used between client and Kanidm."; default = false; }; adminsGroup = mkOption { - type = - types.nullOr (lib.types.strMatching "sp\.[A-Za-z0-9]+\.admins"); - description = - "Name of admins group in Kanidm, whose members have admin level access to resources (service) associated with OAuth2 client authorization."; + type = types.nullOr (lib.types.strMatching "sp\.[A-Za-z0-9]+\.admins"); + description = "Name of admins group in Kanidm, whose members have admin level access to resources (service) associated with OAuth2 client authorization."; default = null; }; usersGroup = mkOption { - type = - types.nullOr (lib.types.strMatching "sp\.[A-Za-z0-9]+\.users"); - description = - "Name of users group in Kanidm, whose members have user level access to resources (service) associated with OAuth2 client authorization."; + type = types.nullOr (lib.types.strMatching "sp\.[A-Za-z0-9]+\.users"); + description = "Name of users group in Kanidm, whose members have user level access to resources (service) associated with OAuth2 client authorization."; default = null; }; originLanding = mkOption { type = types.nullOr lib.types.str; - description = - "The origin landing of the service for OAuth2 redirects."; + description = "The origin landing of the service for OAuth2 redirects."; }; originUrl = mkOption { type = types.nullOr lib.types.str; - description = - "The origin URL of the service for OAuth2 redirects."; + description = "The origin URL of the service for OAuth2 redirects."; }; subdomain = lib.mkOption { - type = - lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; + type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]"; description = "Subdomain of the service."; }; # when true, "name" is passed to a service instead of "name@domain" useShortPreferredUsername = mkOption { - description = - "Use 'name' instead of 'spn' in the preferred_username claim."; + description = "Use 'name' instead of 'spn' in the preferred_username claim."; type = types.bool; default = true; }; linuxUserOfClient = mkOption { type = types.nullOr lib.types.str; - description = - "Name of a Linux OAuth2 client user, under which it should get access through a folder with keys."; + description = "Name of a Linux OAuth2 client user, under which it should get access through a folder with keys."; default = null; }; linuxGroupOfClient = mkOption { type = types.nullOr lib.types.str; - description = - "Name of Linux OAuth2 client group, under which it should read an OAuth2 client secret file."; + description = "Name of Linux OAuth2 client group, under which it should read an OAuth2 client secret file."; default = null; }; isTokenNeeded = mkOption { - description = - "Whether a read-only needs to be generated for LDAP access."; + description = "Whether a read-only needs to be generated for LDAP access."; type = types.bool; default = false; }; clientSystemdUnits = mkOption { description = "A list of systemd services, which depend on OAuth service"; # taken from nixos/lib/systemd-lib.nix: unitNameType - type = types.listOf - (types.strMatching "[a-zA-Z0-9@%:_.\\-]+[.](service|socket|device|mount|automount|swap|target|path|timer|scope|slice)"); + type = types.listOf ( + types.strMatching "[a-zA-Z0-9@%:_.\\-]+[.](service|socket|device|mount|automount|swap|target|path|timer|scope|slice)" + ); }; scopeMaps = mkOption { description = '' @@ -231,133 +223,132 @@ in }; config = lib.mkIf config.selfprivacy.sso.enable ( let - clientsAttrsList = lib.attrsets.mapAttrsToList - (name: attrs: attrs // rec { - clientID = - if attrs.clientID == null - then name - else attrs.clientID; - displayName = - if attrs.displayName == null - then clientID - else attrs.displayName; - adminsGroup = - if attrs.adminsGroup == null - then "sp.${clientID}.admins" - else attrs.adminsGroup; - usersGroup = - if attrs.usersGroup == null - then "sp.${clientID}.users" - else attrs.usersGroup; - basicSecretFile = - "${keys-path}/${linuxGroupOfClient}/kanidm-oauth-client-secret"; - linuxUserOfClient = - if attrs.linuxUserOfClient == null - then clientID - else attrs.linuxUserOfClient; + clientsAttrsList = lib.attrsets.mapAttrsToList ( + name: attrs: + attrs + // rec { + clientID = if attrs.clientID == null then name else attrs.clientID; + displayName = if attrs.displayName == null then clientID else attrs.displayName; + adminsGroup = if attrs.adminsGroup == null then "sp.${clientID}.admins" else attrs.adminsGroup; + usersGroup = if attrs.usersGroup == null then "sp.${clientID}.users" else attrs.usersGroup; + basicSecretFile = "${keys-path}/${linuxGroupOfClient}/kanidm-oauth-client-secret"; + linuxUserOfClient = if attrs.linuxUserOfClient == null then clientID else attrs.linuxUserOfClient; linuxGroupOfClient = - if attrs.linuxGroupOfClient == null - then clientID - else attrs.linuxGroupOfClient; + if attrs.linuxGroupOfClient == null then clientID else attrs.linuxGroupOfClient; originLanding = - if attrs.originLanding == null - then "https://${attrs.subdomain}.${config.selfprivacy.domain}/" - else attrs.originLanding; + if attrs.originLanding == null then + "https://${attrs.subdomain}.${config.selfprivacy.domain}/" + else + attrs.originLanding; scopeMaps = - if attrs.scopeMaps == null - then { "${usersGroup}" = [ "email" "openid" "profile" ]; } - else attrs.scopeMaps; - }) - config.selfprivacy.auth.clients; + if attrs.scopeMaps == null then + { + "${usersGroup}" = [ + "email" + "openid" + "profile" + ]; + } + else + attrs.scopeMaps; + } + ) config.selfprivacy.auth.clients; in { # for each OAuth2 client: member of the `keys` group for directory access - users.groups.keys.members = lib.mkMerge (lib.forEach - clientsAttrsList - ({ linuxUserOfClient, ... }: [ linuxUserOfClient ]) + users.groups.keys.members = lib.mkMerge ( + lib.forEach clientsAttrsList ({ linuxUserOfClient, ... }: [ linuxUserOfClient ]) ); - systemd.tmpfiles.settings."kanidm-secrets" = lib.mkMerge (lib.forEach - clientsAttrsList - ({ linuxGroupOfClient, ... }: { - "${keys-path}/${linuxGroupOfClient}".d = { - user = "kanidm"; - group = linuxGroupOfClient; - mode = "2750"; - }; - }) + systemd.tmpfiles.settings."kanidm-secrets" = lib.mkMerge ( + lib.forEach clientsAttrsList ( + { linuxGroupOfClient, ... }: + { + "${keys-path}/${linuxGroupOfClient}".d = { + user = "kanidm"; + group = linuxGroupOfClient; + mode = "2750"; + }; + } + ) ); # for each OAuth2 client: scripts with Kanidm CLI commands systemd.services.kanidm = { - before = - lib.lists.concatMap - ({ clientSystemdUnits, ... }: clientSystemdUnits) - clientsAttrsList; - serviceConfig = - lib.mkMerge (lib.forEach - clientsAttrsList - ({ clientID, isTokenNeeded, linuxGroupOfClient, isMailserver, ... }: - { - ExecStartPre = [ - # "-" prefix means to ignore exit code of prefixed script - ("-" + mkKanidmExecStartPreScript clientID linuxGroupOfClient) - ]; - ExecStartPost = lib.mkIf isTokenNeeded - (lib.mkAfter [ - ("-" + - mkKanidmExecStartPostScript - clientID - linuxGroupOfClient - isMailserver) - ]); - })); + before = lib.lists.concatMap ({ clientSystemdUnits, ... }: clientSystemdUnits) clientsAttrsList; + serviceConfig = lib.mkMerge ( + lib.forEach clientsAttrsList ( + { + clientID, + isTokenNeeded, + linuxGroupOfClient, + isMailserver, + ... + }: + { + ExecStartPre = [ + # "-" prefix means to ignore exit code of prefixed script + ("-" + mkKanidmExecStartPreScript clientID linuxGroupOfClient) + ]; + ExecStartPost = lib.mkIf isTokenNeeded ( + lib.mkAfter [ + ("-" + mkKanidmExecStartPostScript clientID linuxGroupOfClient isMailserver) + ] + ); + } + ) + ); }; # for each OAuth2 client: Kanidm provisioning options - services.kanidm.provision = lib.mkMerge (lib.forEach - clientsAttrsList - ({ adminsGroup - , basicSecretFile - , claimMaps - , clientID - , displayName - , enablePkce - , imageFile - , originLanding - , originUrl - , scopeMaps - , useShortPreferredUsername - , usersGroup - , ... - }: { - groups = lib.mkIf (clientID != "selfprivacy-api") { - "${adminsGroup}".members = - [ auth-passthru.admins-group ]; - "${usersGroup}".members = - [ adminsGroup auth-passthru.full-users-group ]; - }; - systems.oauth2.${clientID} = { - inherit - basicSecretFile - claimMaps - displayName - imageFile - originLanding - originUrl - scopeMaps - ; - preferShortUsername = useShortPreferredUsername; - allowInsecureClientDisablePkce = ! enablePkce; - removeOrphanedClaimMaps = true; + services.kanidm.provision = lib.mkMerge ( + lib.forEach clientsAttrsList ( + { + adminsGroup, + basicSecretFile, + claimMaps, + clientID, + displayName, + enablePkce, + imageFile, + originLanding, + originUrl, + scopeMaps, + useShortPreferredUsername, + usersGroup, + ... + }: + { + groups = lib.mkIf (clientID != "selfprivacy-api") { + "${adminsGroup}".members = [ auth-passthru.admins-group ]; + "${usersGroup}".members = [ + adminsGroup + auth-passthru.full-users-group + ]; + }; + systems.oauth2.${clientID} = { + inherit + basicSecretFile + claimMaps + displayName + imageFile + originLanding + originUrl + scopeMaps + ; + preferShortUsername = useShortPreferredUsername; + allowInsecureClientDisablePkce = !enablePkce; + removeOrphanedClaimMaps = true; - # NOTE https://github.com/oddlama/kanidm-provision/issues/15 - # add more scopes when a user is a member of specific group - # currently not possible due to https://github.com/kanidm/kanidm/issues/2882#issuecomment-2564490144 - # supplementaryScopeMaps."${admins-group}" = - # [ "read:admin" "write:admin" ]; - }; - })); + # NOTE https://github.com/oddlama/kanidm-provision/issues/15 + # add more scopes when a user is a member of specific group + # currently not possible due to https://github.com/kanidm/kanidm/issues/2882#issuecomment-2564490144 + # supplementaryScopeMaps."${admins-group}" = + # [ "read:admin" "write:admin" ]; + }; + } + ) + ); } ); } diff --git a/auth/auth.nix b/auth/auth.nix index ca1e6bb..09f1a22 100644 --- a/auth/auth.nix +++ b/auth/auth.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let domain = config.selfprivacy.domain; subdomain = "auth"; @@ -38,7 +43,6 @@ lib.mkIf config.selfprivacy.sso.enable { "127.0.0.1" = [ auth-fqdn ]; }; - # kanidm uses TLS in internal connection with nginx too # FIXME revise this: maybe kanidm must not have access to a public TLS users.groups."acmereceivers".members = [ "kanidm" ]; @@ -69,16 +73,13 @@ lib.mkIf config.selfprivacy.sso.enable { origin = "https://" + auth-fqdn; # TODO revise this: maybe kanidm must not have access to a public TLS - tls_chain = - "${config.security.acme.certs.${domain}.directory}/fullchain.pem"; - tls_key = - "${config.security.acme.certs.${domain}.directory}/key.pem"; + tls_chain = "${config.security.acme.certs.${domain}.directory}/fullchain.pem"; + tls_key = "${config.security.acme.certs.${domain}.directory}/key.pem"; # nginx should proxy requests to it bindaddress = kanidm-bind-address; - ldapbindaddress = - "${ldap-host}:${toString ldap-port}"; + ldapbindaddress = "${ldap-host}:${toString ldap-port}"; # kanidm is behind a proxy trust_x_forward_for = true; @@ -101,8 +102,7 @@ lib.mkIf config.selfprivacy.sso.enable { services.nginx = { enable = true; - additionalModules = - lib.mkIf config.selfprivacy.sso.debug [ pkgs.nginxModules.lua ]; + additionalModules = lib.mkIf config.selfprivacy.sso.debug [ pkgs.nginxModules.lua ]; commonHttpConfig = lib.mkIf config.selfprivacy.sso.debug '' log_format kanidm escape=none '$request $status\n' '[Request body]: $request_body\n' @@ -158,8 +158,7 @@ lib.mkIf config.selfprivacy.sso.enable { systemd.services.kanidm.serviceConfig.ExecStartPre = # idempotent script to run on each startup only for kanidm v1.5.0 - lib.mkIf (pkgs.kanidm.version == "1.5.0") - (lib.mkBefore [ kanidmMigrateDbScript ]); + lib.mkIf (pkgs.kanidm.version == "1.5.0") (lib.mkBefore [ kanidmMigrateDbScript ]); selfprivacy.passthru.auth = { inherit @@ -171,25 +170,20 @@ lib.mkIf config.selfprivacy.sso.enable { keys-path ; oauth2-introspection-url-prefix = client_id: "https://${client_id}:"; - oauth2-introspection-url-postfix = - "@${auth-fqdn}/oauth2/token/introspect"; - oauth2-discovery-url = client_id: - "https://${auth-fqdn}/oauth2/openid/${client_id}/.well-known/openid-configuration"; + oauth2-introspection-url-postfix = "@${auth-fqdn}/oauth2/token/introspect"; + oauth2-discovery-url = + client_id: "https://${auth-fqdn}/oauth2/openid/${client_id}/.well-known/openid-configuration"; oauth2-provider-name = "Kanidm"; oauth2-systemd-service = "kanidm.service"; # e.g. "dc=mydomain,dc=com" - ldap-base-dn = - lib.strings.concatMapStringsSep - "," - (x: "dc=" + x) - (lib.strings.splitString "." domain); + ldap-base-dn = lib.strings.concatMapStringsSep "," (x: "dc=" + x) ( + lib.strings.splitString "." domain + ); # TODO consider to pass a value or throw exception if token is not generated - mkServiceAccountTokenFP = linuxGroup: - "${keys-path}/${linuxGroup}/kanidm-service-account-token"; + mkServiceAccountTokenFP = linuxGroup: "${keys-path}/${linuxGroup}/kanidm-service-account-token"; - mkOAuth2ClientSecretFP = linuxGroup: - "${keys-path}/${linuxGroup}/kanidm-oauth-client-secret"; + mkOAuth2ClientSecretFP = linuxGroup: "${keys-path}/${linuxGroup}/kanidm-oauth-client-secret"; }; } diff --git a/auth/kanidm.nix b/auth/kanidm.nix index 7290374..ddf7f81 100644 --- a/auth/kanidm.nix +++ b/auth/kanidm.nix @@ -1,8 +1,9 @@ -{ config -, lib -, options -, pkgs -, ... +{ + config, + lib, + options, + pkgs, + ... }: let inherit (lib) @@ -40,7 +41,9 @@ let cfg = config.services.kanidm; settingsFormat = pkgs.formats.toml { }; # Remove null values, so we can document optional values that don't end up in the generated TOML file. - filterConfig = converge (a: filterAttrsRecursive (_: v: v != null) (builtins.removeAttrs a [ "provision" ])); + filterConfig = converge ( + a: filterAttrsRecursive (_: v: v != null) (builtins.removeAttrs a [ "provision" ]) + ); serverConfigFile = settingsFormat.generate "server.toml" (filterConfig cfg.serverSettings); clientConfigFile = settingsFormat.generate "kanidm-config.toml" (filterConfig cfg.clientSettings); unixConfigFile = settingsFormat.generate "kanidm-unixd.toml" (filterConfig cfg.unixSettings); @@ -54,17 +57,16 @@ let # paths, no new bind mount is added. Adding subpaths caused problems on ofborg. hasPrefixInList = list: newPath: any (path: hasPrefix (builtins.toString path) (builtins.toString newPath)) list; - mergePaths = foldl' - ( - merged: newPath: - let - # If the new path is a prefix to some existing path, we need to filter it out - filteredPaths = filter (p: !hasPrefix (builtins.toString newPath) (builtins.toString p)) merged; - # If a prefix of the new path is already in the list, do not add it - filteredNew = optional (!hasPrefixInList filteredPaths newPath) newPath; - in - filteredPaths ++ filteredNew - ) [ ]; + mergePaths = foldl' ( + merged: newPath: + let + # If the new path is a prefix to some existing path, we need to filter it out + filteredPaths = filter (p: !hasPrefix (builtins.toString newPath) (builtins.toString p)) merged; + # If a prefix of the new path is already in the list, do not add it + filteredNew = optional (!hasPrefixInList filteredPaths newPath) newPath; + in + filteredPaths ++ filteredNew + ) [ ]; defaultServiceConfig = { # Setting the type to notify enables additional healthchecks, ensuring units @@ -127,19 +129,20 @@ let filterPresent = filterAttrs (_: v: v.present); selfprivacy-admin-groups-regex = "^sp\.([[:alnum:]]+\.|)admins$"; - is-selfprivacy-admin-group = name: - ! builtins.isNull (builtins.match selfprivacy-admin-groups-regex name); + is-selfprivacy-admin-group = + name: !builtins.isNull (builtins.match selfprivacy-admin-groups-regex name); - isGroupNonOverwritable = g: false - || ! g ? members + isGroupNonOverwritable = + g: + false + || !g ? members || g ? members && g.members == [ ] || g ? members && builtins.any is-selfprivacy-admin-group g.members; provisionStateJson = pkgs.writeText "provision-state.json" ( builtins.toJSON { inherit (cfg.provision) persons systems; - groups = - lib.attrsets.filterAttrs (_n: v: ! isGroupNonOverwritable v) cfg.provision.groups; + groups = lib.attrsets.filterAttrs (_n: v: !isGroupNonOverwritable v) cfg.provision.groups; } ); @@ -182,8 +185,9 @@ let fi ''; - groupsToCreateAndPopulate = - lib.attrsets.filterAttrs (_n: isGroupNonOverwritable) cfg.provision.groups; + groupsToCreateAndPopulate = lib.attrsets.filterAttrs ( + _n: isGroupNonOverwritable + ) cfg.provision.groups; createGroups = '' for group_name in ${lib.strings.concatStringsSep " " (builtins.attrNames groupsToCreateAndPopulate)} @@ -199,9 +203,9 @@ let done ''; - createAndPopulateGroups = - lib.concatLines ([ createGroups ] - ++ (lib.mapAttrsToList populateGroup groupsToCreateAndPopulate)); + createAndPopulateGroups = lib.concatLines ( + [ createGroups ] ++ (lib.mapAttrsToList populateGroup groupsToCreateAndPopulate) + ); postStartScript = pkgs.writeShellScript "post-start" '' set -euo pipefail @@ -250,11 +254,11 @@ let last (splitString "]:" cfg.serverSettings.bindaddress) else # ipv4: - if hasInfix "." cfg.serverSettings.bindaddress then - last (splitString ":" cfg.serverSettings.bindaddress) - # default is 8443 - else - "8443"; + if hasInfix "." cfg.serverSettings.bindaddress then + last (splitString ":" cfg.serverSettings.bindaddress) + # default is 8443 + else + "8443"; in { options.services.kanidm = { @@ -476,11 +480,9 @@ in config.members = concatLists ( flip mapAttrsToList cfg.provision.persons ( person: personCfg: - optional - ( - personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups - ) - person + optional ( + personCfg.present && builtins.elem groupSubmod.config._module.args.name personCfg.groups + ) person ) ); }) @@ -683,12 +685,9 @@ in ++ entityList "oauth2" cfg.provision.systems.oauth2; # Accumulate entities by name. Track corresponding entity types for later duplicate check. - entitiesByName = foldl' - ( - acc: { type, name }: acc // { ${name} = (acc.${name} or [ ]) ++ [ type ]; } - ) - { } - entities; + entitiesByName = foldl' ( + acc: { type, name }: acc // { ${name} = (acc.${name} or [ ]) ++ [ type ]; } + ) { } entities; assertGroupsKnown = opt: groups: @@ -800,59 +799,59 @@ in ] ++ flip mapAttrsToList (filterPresent cfg.provision.persons) ( person: personCfg: - assertGroupsKnown "services.kanidm.provision.persons.${person}.groups" personCfg.groups + assertGroupsKnown "services.kanidm.provision.persons.${person}.groups" personCfg.groups ) ++ flip mapAttrsToList (filterPresent cfg.provision.groups) ( group: groupCfg: - assertEntitiesKnown "services.kanidm.provision.groups.${group}.members" groupCfg.members + assertEntitiesKnown "services.kanidm.provision.groups.${group}.members" groupCfg.members ) ++ concatLists ( flip mapAttrsToList (filterPresent cfg.provision.systems.oauth2) ( oauth2: oauth2Cfg: - [ - (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.scopeMaps" ( - attrNames oauth2Cfg.scopeMaps - )) - (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" ( - attrNames oauth2Cfg.supplementaryScopeMaps - )) - ] - ++ concatLists ( - flip mapAttrsToList oauth2Cfg.claimMaps ( - claim: claimCfg: [ - (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" ( - attrNames claimCfg.valuesByGroup - )) - # At least one group must map to a value in each claim map - { - assertion = - (cfg.provision.enable && cfg.enableServer) - -> any (xs: xs != [ ]) (attrValues claimCfg.valuesByGroup); - message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group"; - } - # Public clients cannot define a basic secret - { - assertion = - (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> oauth2Cfg.basicSecretFile == null; - message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot specify a basic secret"; - } - # Public clients cannot disable PKCE - { - assertion = - (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) - -> !oauth2Cfg.allowInsecureClientDisablePkce; - message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot disable PKCE"; - } - # Non-public clients cannot enable localhost redirects - { - assertion = - (cfg.provision.enable && cfg.enableServer && !oauth2Cfg.public) - -> !oauth2Cfg.enableLocalhostRedirects; - message = "services.kanidm.provision.systems.oauth2.${oauth2} is a non-public client and thus cannot enable localhost redirects"; - } - ] - ) + [ + (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.scopeMaps" ( + attrNames oauth2Cfg.scopeMaps + )) + (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.supplementaryScopeMaps" ( + attrNames oauth2Cfg.supplementaryScopeMaps + )) + ] + ++ concatLists ( + flip mapAttrsToList oauth2Cfg.claimMaps ( + claim: claimCfg: [ + (assertGroupsKnown "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim}.valuesByGroup" ( + attrNames claimCfg.valuesByGroup + )) + # At least one group must map to a value in each claim map + { + assertion = + (cfg.provision.enable && cfg.enableServer) + -> any (xs: xs != [ ]) (attrValues claimCfg.valuesByGroup); + message = "services.kanidm.provision.systems.oauth2.${oauth2}.claimMaps.${claim} does not specify any values for any group"; + } + # Public clients cannot define a basic secret + { + assertion = + (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) -> oauth2Cfg.basicSecretFile == null; + message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot specify a basic secret"; + } + # Public clients cannot disable PKCE + { + assertion = + (cfg.provision.enable && cfg.enableServer && oauth2Cfg.public) + -> !oauth2Cfg.allowInsecureClientDisablePkce; + message = "services.kanidm.provision.systems.oauth2.${oauth2} is a public client and thus cannot disable PKCE"; + } + # Non-public clients cannot enable localhost redirects + { + assertion = + (cfg.provision.enable && cfg.enableServer && !oauth2Cfg.public) + -> !oauth2Cfg.enableLocalhostRedirects; + message = "services.kanidm.provision.systems.oauth2.${oauth2} is a non-public client and thus cannot enable localhost redirects"; + } + ] ) + ) ) ); diff --git a/configuration.nix b/configuration.nix index 4fc9baa..8005c01 100644 --- a/configuration.nix +++ b/configuration.nix @@ -1,4 +1,9 @@ -{ config, pkgs, lib, ... }: +{ + config, + pkgs, + lib, + ... +}: let redis-sp-api-srv-name = "sp-api"; sp-print-api-token = pkgs.writeShellApplication { @@ -76,7 +81,8 @@ in }; }; - services.do-agent.enable = if config.selfprivacy.server.provider == "DIGITALOCEAN" then true else false; + services.do-agent.enable = + if config.selfprivacy.server.provider == "DIGITALOCEAN" then true else false; boot.tmp.cleanOnBoot = true; networking = { @@ -84,14 +90,31 @@ in domain = config.selfprivacy.domain; usePredictableInterfaceNames = false; firewall = { - allowedTCPPorts = [ 22 25 80 143 443 465 587 993 4443 8443 ]; - allowedUDPPorts = [ 8443 10000 ]; + allowedTCPPorts = [ + 22 + 25 + 80 + 143 + 443 + 465 + 587 + 993 + 4443 + 8443 + ]; + allowedUDPPorts = [ + 8443 + 10000 + ]; extraCommands = '' iptables --table nat --append POSTROUTING --out-interface eth0 -j MASQUERADE iptables --append FORWARD --in-interface vpn00 -j ACCEPT ''; }; - nameservers = [ "1.1.1.1" "1.0.0.1" ]; + nameservers = [ + "1.1.1.1" + "1.0.0.1" + ]; }; time.timeZone = config.selfprivacy.timezone; i18n.defaultLocale = "en_GB.UTF-8"; @@ -107,8 +130,15 @@ in }; services.fail2ban.enable = true; programs.ssh = { - pubkeyAcceptedKeyTypes = [ "ssh-ed25519" "ssh-rsa" "ecdsa-sha2-nistp256" ]; - hostKeyAlgorithms = [ "ssh-ed25519" "ssh-rsa" ]; + pubkeyAcceptedKeyTypes = [ + "ssh-ed25519" + "ssh-rsa" + "ecdsa-sha2-nistp256" + ]; + hostKeyAlgorithms = [ + "ssh-ed25519" + "ssh-rsa" + ]; }; environment.systemPackages = with pkgs; [ git @@ -124,9 +154,9 @@ in "R! /old-root" "d /etc/selfprivacy/dump 0700 0700 selfprivacy-api selfprivacy-api" ]; - system.stateVersion = - lib.mkIf (config.selfprivacy.stateVersion != null) - config.selfprivacy.stateVersion; + system.stateVersion = lib.mkIf ( + config.selfprivacy.stateVersion != null + ) config.selfprivacy.stateVersion; system.autoUpgrade = { enable = config.selfprivacy.autoUpgrade.enable; allowReboot = config.selfprivacy.autoUpgrade.allowReboot; @@ -168,7 +198,11 @@ in }; nix.settings = { sandbox = true; - experimental-features = [ "nix-command" "flakes" "repl-flake" ]; + experimental-features = [ + "nix-command" + "flakes" + "repl-flake" + ]; # auto-optimise-store = true; # evaluation restrictions: diff --git a/flake.nix b/flake.nix index d18df25..786a668 100644 --- a/flake.nix +++ b/flake.nix @@ -2,148 +2,156 @@ description = "SelfPrivacy NixOS configuration flake"; inputs = { - nixpkgs.url = github:nixos/nixpkgs; - nixos-unstable.url = github:nixos/nixpkgs/nixos-unstable; + nixpkgs.url = "github:nixos/nixpkgs"; + nixos-unstable.url = "github:nixos/nixpkgs/nixos-unstable"; - selfprivacy-api.url = - git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git; + selfprivacy-api.url = "git+https://git.selfprivacy.org/SelfPrivacy/selfprivacy-rest-api.git"; # make selfprivacy-api use the same shared nixpkgs selfprivacy-api.inputs.nixpkgs.follows = "nixpkgs"; }; - outputs = { self, nixpkgs, nixos-unstable, selfprivacy-api }: { - nixosConfigurations-fun = - { hardware-configuration - , deployment - , userdata - , top-level-flake - , sp-modules - }: - { - default = nixpkgs.lib.nixosSystem { - modules = [ - hardware-configuration - deployment - ./configuration.nix - ./auth/auth.nix - { - nixpkgs.overlays = [ - ( - _final: prev: - { + outputs = + { + self, + nixpkgs, + nixos-unstable, + selfprivacy-api, + }: + { + nixosConfigurations-fun = + { + hardware-configuration, + deployment, + userdata, + top-level-flake, + sp-modules, + }: + { + default = nixpkgs.lib.nixosSystem { + modules = + [ + hardware-configuration + deployment + ./configuration.nix + ./auth/auth.nix + { + nixpkgs.overlays = [ + (_final: prev: { inherit (nixos-unstable.legacyPackages.${prev.system}) kanidm kanidm-provision ; - } + }) + ]; + disabledModules = [ "services/security/kanidm.nix" ]; + imports = [ ./auth/kanidm.nix ]; + } + selfprivacy-api.nixosModules.default + ( + { pkgs, lib, ... }: + { + environment.etc = + (lib.attrsets.mapAttrs' (name: sp-module: { + name = "sp-modules/${name}"; + value.text = import ./lib/meta.nix { inherit pkgs sp-module; }; + }) sp-modules) + // { + suggested-sp-modules.text = builtins.toJSON (builtins.attrNames (builtins.readDir ./sp-modules)); + }; + } ) - ]; - disabledModules = [ "services/security/kanidm.nix" ]; - imports = [ ./auth/kanidm.nix ]; - } - selfprivacy-api.nixosModules.default - ({ pkgs, lib, ... }: { - environment.etc = (lib.attrsets.mapAttrs' - (name: sp-module: { - name = "sp-modules/${name}"; - value.text = import ./lib/meta.nix { inherit pkgs sp-module; }; - }) - sp-modules) // { - suggested-sp-modules.text = builtins.toJSON (builtins.attrNames (builtins.readDir ./sp-modules)); - }; - }) - ( - let - deepFilter = ref: attrset: - builtins.foldl' - (acc: key: - if builtins.hasAttr key ref then - let - value = attrset.${key}; - refValue = ref.${key}; - in - acc // { - ${key} = - if builtins.isAttrs value && builtins.isAttrs refValue then - deepFilter refValue value - else - value; - } - else - acc - ) - { } - (builtins.attrNames attrset); - in - { options, ... }: { - # pass userdata (parsed from JSON) options to selfprivacy module - selfprivacy = deepFilter options.selfprivacy userdata; + ( + let + deepFilter = + ref: attrset: + builtins.foldl' ( + acc: key: + if builtins.hasAttr key ref then + let + value = attrset.${key}; + refValue = ref.${key}; + in + acc + // { + ${key} = + if builtins.isAttrs value && builtins.isAttrs refValue then deepFilter refValue value else value; + } + else + acc + ) { } (builtins.attrNames attrset); + in + { options, ... }: + { + # pass userdata (parsed from JSON) options to selfprivacy module + selfprivacy = deepFilter options.selfprivacy userdata; - # embed top-level flake source folder into the build - environment.etc."selfprivacy/nixos-config-source".source = - top-level-flake; + # embed top-level flake source folder into the build + environment.etc."selfprivacy/nixos-config-source".source = top-level-flake; - # for running "nix search nixpkgs", "nix shell nixpkgs#PKG... etc - nix.registry.nixpkgs.flake = nixpkgs; + # for running "nix search nixpkgs", "nix shell nixpkgs#PKG... etc + nix.registry.nixpkgs.flake = nixpkgs; - # embed commit sha1 for `nixos-version --configuration-revision` - system.configurationRevision = self.rev - or "@${self.lastModifiedDate}"; # for development - # TODO assertion to forbid dirty builds caused by top-level-flake + # embed commit sha1 for `nixos-version --configuration-revision` + system.configurationRevision = self.rev or "@${self.lastModifiedDate}"; # for development + # TODO assertion to forbid dirty builds caused by top-level-flake - # reset contents of /etc/nixos to match running NixOS generation - system.activationScripts.selfprivacy-nixos-config-source = '' - rm -rf /etc/nixos/{*,.[!.]*} - cp -r --no-preserve=all ${top-level-flake}/ -T /etc/nixos/ - ''; - } - ) - ] - ++ - # add SP modules, but constrain available config attributes for each - # (TODO revise evaluation performance of the code below) - nixpkgs.lib.attrsets.mapAttrsToList - (name: sp-module: args@{ config, pkgs, ... }: - let - lib = nixpkgs.lib; - configPathsNeeded = sp-module.configPathsNeeded or - (abort "allowed config paths not set for module \"${name}\""); - constrainConfigArgs = args'@{ pkgs, ... }: args' // { - config = - # TODO use lib.attrsets.mergeAttrsList from nixpkgs 23.05 - (builtins.foldl' lib.attrsets.recursiveUpdate { } - (map - (p: lib.attrsets.setAttrByPath p - (lib.attrsets.getAttrFromPath p config)) - configPathsNeeded - ) - ); - }; - constrainImportsArgsRecursive = lib.attrsets.mapAttrsRecursive - (p: v: + # reset contents of /etc/nixos to match running NixOS generation + system.activationScripts.selfprivacy-nixos-config-source = '' + rm -rf /etc/nixos/{*,.[!.]*} + cp -r --no-preserve=all ${top-level-flake}/ -T /etc/nixos/ + ''; + } + ) + ] + ++ + # add SP modules, but constrain available config attributes for each + # (TODO revise evaluation performance of the code below) + nixpkgs.lib.attrsets.mapAttrsToList ( + name: sp-module: + args@{ config, pkgs, ... }: + let + lib = nixpkgs.lib; + configPathsNeeded = + sp-module.configPathsNeeded or (abort "allowed config paths not set for module \"${name}\""); + constrainConfigArgs = + args'@{ pkgs, ... }: + args' + // { + config = + # TODO use lib.attrsets.mergeAttrsList from nixpkgs 23.05 + ( + builtins.foldl' lib.attrsets.recursiveUpdate { } ( + map (p: lib.attrsets.setAttrByPath p (lib.attrsets.getAttrFromPath p config)) configPathsNeeded + ) + ); + }; + constrainImportsArgsRecursive = lib.attrsets.mapAttrsRecursive ( + p: v: # TODO traverse only imports and imports of imports, etc # without traversing all attributes - if lib.lists.last p == "imports" - then - map - (m: - (args'@{ pkgs, ... }: constrainImportsArgsRecursive - (if builtins.isPath m - then import m (constrainConfigArgs args') + if lib.lists.last p == "imports" then + map ( + m: + ( + args'@{ pkgs, ... }: + constrainImportsArgsRecursive ( + if builtins.isPath m then + import m (constrainConfigArgs args') + else if builtins.isFunction m then + m (constrainConfigArgs args') else - if builtins.isFunction m - then m (constrainConfigArgs args') - else m)) + m + ) ) - v - else v); - in - constrainImportsArgsRecursive - (sp-module.nixosModules.default (constrainConfigArgs args)) - ) - sp-modules; + ) v + else + v + ); + in + constrainImportsArgsRecursive (sp-module.nixosModules.default (constrainConfigArgs args)) + ) sp-modules; + }; }; - }; - formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixpkgs-fmt; - }; + formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixpkgs-fmt; + }; } diff --git a/letsencrypt/acme.nix b/letsencrypt/acme.nix index 8012bf8..35c5398 100644 --- a/letsencrypt/acme.nix +++ b/letsencrypt/acme.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.selfprivacy; dnsCredentialsTemplates = { @@ -27,7 +32,11 @@ in acceptTerms = true; defaults = { email = "${cfg.username}@${cfg.domain}"; - server = if cfg.dns.useStagingACME then "https://acme-staging-v02.api.letsencrypt.org/directory" else "https://acme-v02.api.letsencrypt.org/directory"; + server = + if cfg.dns.useStagingACME then + "https://acme-staging-v02.api.letsencrypt.org/directory" + else + "https://acme-v02.api.letsencrypt.org/directory"; reloadServices = [ "nginx" ]; dnsResolver = "8.8.8.8:53"; }; @@ -38,7 +47,9 @@ in dnsProvider = lib.strings.toLower cfg.dns.provider; credentialsFile = acme-env-filepath; dnsPropagationCheck = - ! ((lib.elem cfg.dns.provider dnsPropagationCheckExceptions) || cfg.dns.forceDisableDnsPropagationCheck); + !( + (lib.elem cfg.dns.provider dnsPropagationCheckExceptions) || cfg.dns.forceDisableDnsPropagationCheck + ); }; "root-${cfg.domain}" = { domain = cfg.domain; @@ -51,7 +62,10 @@ in before = [ "acme-${cfg.domain}.service" ]; requiredBy = [ "acme-${cfg.domain}.service" ]; serviceConfig.Type = "oneshot"; - path = with pkgs; [ coreutils jq ]; + path = with pkgs; [ + coreutils + jq + ]; script = '' set -o nounset diff --git a/lib/meta.nix b/lib/meta.nix index fcfc186..e22e78d 100644 --- a/lib/meta.nix +++ b/lib/meta.nix @@ -1,18 +1,28 @@ { sp-module, pkgs }: let lib = pkgs.lib; - options = (pkgs.lib.evalModules { modules = [{ _module.check = false; } sp-module.nixosModules.default]; }).options; + options = + (pkgs.lib.evalModules { + modules = [ + { _module.check = false; } + sp-module.nixosModules.default + ]; + }).options; # Transform a Nix option to a JSON structure with metadata - optionToMeta = (name: option: { - name = name; - description = if builtins.hasAttr "description" option then option.description else null; - loc = option.loc; - meta = if builtins.hasAttr "meta" option then option.meta else null; - default = if builtins.hasAttr "default" option then option.default else null; - }); + optionToMeta = ( + name: option: { + name = name; + description = if builtins.hasAttr "description" option then option.description else null; + loc = option.loc; + meta = if builtins.hasAttr "meta" option then option.meta else null; + default = if builtins.hasAttr "default" option then option.default else null; + } + ); in builtins.toJSON ({ meta = if builtins.hasAttr "meta" sp-module then sp-module.meta { inherit lib; } else null; configPathsNeeded = sp-module.configPathsNeeded; - options = pkgs.lib.mapAttrs optionToMeta (builtins.head (lib.mapAttrsToList (name: value: value) options.selfprivacy.modules)); + options = pkgs.lib.mapAttrs optionToMeta ( + builtins.head (lib.mapAttrsToList (name: value: value) options.selfprivacy.modules) + ); }) diff --git a/overlay.nix b/overlay.nix index eb9b7a8..fc734f3 100644 --- a/overlay.nix +++ b/overlay.nix @@ -1,6 +1,4 @@ -system: -_final: _prev: -{ +system: _final: _prev: { # Here is a template to bring a specific package from a given nixpkgs commit: # PACKAGE_NAME = (builtins.getFlake "github:nixos/nixpkgs/NIXPKGS_COMMIT_SHA1").legacyPackages.${system}.PACKAGE_NAME; # Substitute `PACKAGE_NAME` and `NIXPKGS_COMMIT_SHA1` accordingly. diff --git a/postgresql/postgresql.nix b/postgresql/postgresql.nix index e281d7f..df65a82 100644 --- a/postgresql/postgresql.nix +++ b/postgresql/postgresql.nix @@ -1,22 +1,34 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let sp = config.selfprivacy; pleroma_location = - if lib.attrsets.hasAttr "pleroma" sp.modules && lib.attrsets.hasAttr "location" sp.modules.pleroma - then sp.modules.pleroma.location - else null; + if + lib.attrsets.hasAttr "pleroma" sp.modules && lib.attrsets.hasAttr "location" sp.modules.pleroma + then + sp.modules.pleroma.location + else + null; postgres_location = - if lib.attrsets.hasAttr "postgresql" sp && lib.attrsets.hasAttr "location" sp.postgresql - then sp.postgresql.location - else null; + if lib.attrsets.hasAttr "postgresql" sp && lib.attrsets.hasAttr "location" sp.postgresql then + sp.postgresql.location + else + null; # Priority: postgresql > pleroma location = if postgres_location != null then postgres_location else pleroma_location; # Active if there is a location enable = location != null; pleroma_enabled = - if lib.attrsets.hasAttr "pleroma" sp.modules && lib.attrsets.hasAttr "enable" sp.modules.pleroma - then sp.modules.pleroma.enable - else false; + if + lib.attrsets.hasAttr "pleroma" sp.modules && lib.attrsets.hasAttr "enable" sp.modules.pleroma + then + sp.modules.pleroma.enable + else + false; in { imports = [ diff --git a/postgresql/upgrade12to16.nix b/postgresql/upgrade12to16.nix index 9f53d16..72e5177 100644 --- a/postgresql/upgrade12to16.nix +++ b/postgresql/upgrade12to16.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let cfg = config.services.postgresqlUpgrade12to16; @@ -62,72 +67,70 @@ in ExecStartPre = # Stop Pleroma only if pleromaEnabled is true - optional cfg.pleromaEnabled "${pkgs.writeShellScript "postgresql-upgrade12to16-pre.sh" '' - if [ -d "${cfg.dataDir12}" ] && [ ! -d "${cfg.dataDir16}" ]; then - ${pkgs.systemd}/bin/systemctl stop pleroma.service - fi - '' - }"; + optional cfg.pleromaEnabled + "${pkgs.writeShellScript "postgresql-upgrade12to16-pre.sh" '' + if [ -d "${cfg.dataDir12}" ] && [ ! -d "${cfg.dataDir16}" ]; then + ${pkgs.systemd}/bin/systemctl stop pleroma.service + fi + ''}"; ExecStart = "${pkgs.writeShellScript "postgresql-upgrade12to16.sh" '' - set -e + set -e - oldDataDir="${cfg.dataDir12}" - newDataDir="${cfg.dataDir16}" + oldDataDir="${cfg.dataDir12}" + newDataDir="${cfg.dataDir16}" - # Only upgrade if old data directory exists, and the new one doesn't yet - if [ -d "$oldDataDir" ] && [ ! -d "$newDataDir" ]; then - echo "Detected PostgreSQL 12 data directory at $oldDataDir" - echo "Upgrading to PostgreSQL 16 data directory at $newDataDir" + # Only upgrade if old data directory exists, and the new one doesn't yet + if [ -d "$oldDataDir" ] && [ ! -d "$newDataDir" ]; then + echo "Detected PostgreSQL 12 data directory at $oldDataDir" + echo "Upgrading to PostgreSQL 16 data directory at $newDataDir" - # Stop the old PostgreSQL if it's running - if systemctl is-active --quiet postgresql.service; then - systemctl stop postgresql.service - fi - - # Create the new data directory (if not already present) - mkdir -p "$newDataDir" - chown -R postgres:postgres "$(dirname "$newDataDir")" - - # Create a temporary working directory - tempDir=$(mktemp -d) - chown -R postgres:postgres "$tempDir" - trap 'rm -rf "$tempDir"' EXIT - - # Change to the temporary working directory - cd "$tempDir" - - # Initialize the new PostgreSQL 16 data directory - ${pkgs.sudo}/bin/sudo -u postgres ${pkgs.postgresql_16.out}/bin/initdb -D "$newDataDir" -U postgres - - # Run pg_upgrade as the postgres user - ${pkgs.sudo}/bin/sudo -u postgres ${pkgs.postgresql_16.out}/bin/pg_upgrade \ - --old-datadir "$oldDataDir" \ - --new-datadir "$newDataDir" \ - --old-bindir ${pkgs.postgresql_12.out}/bin \ - --new-bindir ${pkgs.postgresql_16.out}/bin \ - --jobs "$(nproc)" \ - --link \ - --verbose - - touch "$newDataDir/.sp_migrated" - - echo "PostgreSQL upgrade from 12 to 16 completed." - else - echo "No PostgreSQL 12 data directory detected or already upgraded. Skipping." + # Stop the old PostgreSQL if it's running + if systemctl is-active --quiet postgresql.service; then + systemctl stop postgresql.service fi - ''}"; + + # Create the new data directory (if not already present) + mkdir -p "$newDataDir" + chown -R postgres:postgres "$(dirname "$newDataDir")" + + # Create a temporary working directory + tempDir=$(mktemp -d) + chown -R postgres:postgres "$tempDir" + trap 'rm -rf "$tempDir"' EXIT + + # Change to the temporary working directory + cd "$tempDir" + + # Initialize the new PostgreSQL 16 data directory + ${pkgs.sudo}/bin/sudo -u postgres ${pkgs.postgresql_16.out}/bin/initdb -D "$newDataDir" -U postgres + + # Run pg_upgrade as the postgres user + ${pkgs.sudo}/bin/sudo -u postgres ${pkgs.postgresql_16.out}/bin/pg_upgrade \ + --old-datadir "$oldDataDir" \ + --new-datadir "$newDataDir" \ + --old-bindir ${pkgs.postgresql_12.out}/bin \ + --new-bindir ${pkgs.postgresql_16.out}/bin \ + --jobs "$(nproc)" \ + --link \ + --verbose + + touch "$newDataDir/.sp_migrated" + + echo "PostgreSQL upgrade from 12 to 16 completed." + else + echo "No PostgreSQL 12 data directory detected or already upgraded. Skipping." + fi + ''}"; # Start Pleroma only if pleromaEnabled is true - ExecStartPost = - optional cfg.pleromaEnabled "${pkgs.writeShellScript "postgresql-upgrade12to16-post.sh" '' - if test -e "${cfg.dataDir16}/.sp_migrated"; then - ${pkgs.systemd}/bin/systemctl start --no-block pleroma.service + ExecStartPost = optional cfg.pleromaEnabled "${pkgs.writeShellScript "postgresql-upgrade12to16-post.sh" '' + if test -e "${cfg.dataDir16}/.sp_migrated"; then + ${pkgs.systemd}/bin/systemctl start --no-block pleroma.service - rm -f "${cfg.dataDir16}/.sp_migrated" - fi - '' - }"; + rm -f "${cfg.dataDir16}/.sp_migrated" + fi + ''}"; }; }; }; diff --git a/sp-modules/bitwarden/cleanup-module.nix b/sp-modules/bitwarden/cleanup-module.nix index d66785f..0e2b5cb 100644 --- a/sp-modules/bitwarden/cleanup-module.nix +++ b/sp-modules/bitwarden/cleanup-module.nix @@ -5,13 +5,8 @@ in # FIXME do we really want to delete passwords on module deactivation!? { config = lib.mkIf (!sp.modules.bitwarden.enable) { - system.activationScripts.bitwarden = - lib.trivial.warn - ( - "bitwarden service is disabled, ${bitwarden-env} will be removed!" - ) - '' - rm -f -v ${bitwarden-env} - ''; + system.activationScripts.bitwarden = lib.trivial.warn ("bitwarden service is disabled, ${bitwarden-env} will be removed!") '' + rm -f -v ${bitwarden-env} + ''; }; } diff --git a/sp-modules/bitwarden/common.nix b/sp-modules/bitwarden/common.nix index 231bed3..1ede8c2 100644 --- a/sp-modules/bitwarden/common.nix +++ b/sp-modules/bitwarden/common.nix @@ -1,5 +1,4 @@ -config: -{ +config: { sp = config.selfprivacy; bitwarden-env = "/var/lib/bitwarden/.env"; } diff --git a/sp-modules/bitwarden/flake.nix b/sp-modules/bitwarden/flake.nix index 478df89..21e824b 100644 --- a/sp-modules/bitwarden/flake.nix +++ b/sp-modules/bitwarden/flake.nix @@ -1,34 +1,41 @@ { description = "PoC SP module for Bitwarden password management solution"; - outputs = { self }: { - nixosModules.default = _: - { imports = [ ./module.nix ./cleanup-module.nix ]; }; - configPathsNeeded = - builtins.fromJSON (builtins.readFile ./config-paths-needed.json); - meta = { lib, ... }: { - spModuleSchemaVersion = 1; - id = "bitwarden"; - name = "Bitwarden"; - description = "Bitwarden is a password manager."; - svgIcon = builtins.readFile ./icon.svg; - isMovable = true; - isRequired = false; - backupDescription = "Password database, encryption certificate and attachments."; - systemdServices = [ - "vaultwarden.service" - ]; - user = "vaultwarden"; - folders = [ - "/var/lib/bitwarden" - "/var/lib/bitwarden_rs" - ]; - license = [ - lib.licenses.agpl3Only - ]; - homepage = "https://github.com/dani-garcia/vaultwarden"; - sourcePage = "https://github.com/dani-garcia/vaultwarden"; - supportLevel = "normal"; + outputs = + { self }: + { + nixosModules.default = _: { + imports = [ + ./module.nix + ./cleanup-module.nix + ]; + }; + configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json); + meta = + { lib, ... }: + { + spModuleSchemaVersion = 1; + id = "bitwarden"; + name = "Bitwarden"; + description = "Bitwarden is a password manager."; + svgIcon = builtins.readFile ./icon.svg; + isMovable = true; + isRequired = false; + backupDescription = "Password database, encryption certificate and attachments."; + systemdServices = [ + "vaultwarden.service" + ]; + user = "vaultwarden"; + folders = [ + "/var/lib/bitwarden" + "/var/lib/bitwarden_rs" + ]; + license = [ + lib.licenses.agpl3Only + ]; + homepage = "https://github.com/dani-garcia/vaultwarden"; + sourcePage = "https://github.com/dani-garcia/vaultwarden"; + supportLevel = "normal"; + }; }; - }; } diff --git a/sp-modules/bitwarden/module.nix b/sp-modules/bitwarden/module.nix index 5fa12ca..0e4b5df 100644 --- a/sp-modules/bitwarden/module.nix +++ b/sp-modules/bitwarden/module.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let secrets-filepath = "/etc/selfprivacy/secrets.json"; backup-dir = "/var/lib/bitwarden/backup"; @@ -7,65 +12,77 @@ let in { options.selfprivacy.modules.bitwarden = { - enable = (lib.mkOption { - default = false; - type = lib.types.bool; - description = "Enable Vaultwarden"; - }) // { - meta = { - type = "enable"; + enable = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable Vaultwarden"; + }) + // { + meta = { + type = "enable"; + }; }; - }; - location = (lib.mkOption { - type = lib.types.str; - description = "Vaultwarden location"; - }) // { - meta = { - type = "location"; + location = + (lib.mkOption { + type = lib.types.str; + description = "Vaultwarden location"; + }) + // { + meta = { + type = "location"; + }; }; - }; - subdomain = (lib.mkOption { - default = "password"; - 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; + subdomain = + (lib.mkOption { + default = "password"; + 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; + }; }; - }; - signupsAllowed = (lib.mkOption { - default = true; - type = lib.types.bool; - description = "Allow new user signups"; - }) // { - meta = { - type = "bool"; - weight = 1; + signupsAllowed = + (lib.mkOption { + default = true; + type = lib.types.bool; + description = "Allow new user signups"; + }) + // { + meta = { + type = "bool"; + weight = 1; + }; }; - }; - sendsAllowed = (lib.mkOption { - default = true; - type = lib.types.bool; - description = "Allow users to use Bitwarden Send"; - }) // { - meta = { - type = "bool"; - weight = 2; + sendsAllowed = + (lib.mkOption { + default = true; + type = lib.types.bool; + description = "Allow users to use Bitwarden Send"; + }) + // { + meta = { + type = "bool"; + weight = 2; + }; }; - }; - emergencyAccessAllowed = (lib.mkOption { - default = true; - type = lib.types.bool; - description = "Allow users to enable Emergency Access"; - }) // { - meta = { - type = "bool"; - weight = 3; + emergencyAccessAllowed = + (lib.mkOption { + default = true; + type = lib.types.bool; + description = "Allow users to enable Emergency Access"; + }) + // { + meta = { + type = "bool"; + weight = 3; + }; }; - }; }; config = lib.mkIf config.selfprivacy.modules.bitwarden.enable { @@ -118,7 +135,10 @@ in before = [ "vaultwarden.service" ]; requiredBy = [ "vaultwarden.service" ]; serviceConfig.Type = "oneshot"; - path = with pkgs; [ coreutils jq ]; + path = with pkgs; [ + coreutils + jq + ]; script = '' set -o nounset diff --git a/sp-modules/gitea/flake.nix b/sp-modules/gitea/flake.nix index f18fa21..decde41 100644 --- a/sp-modules/gitea/flake.nix +++ b/sp-modules/gitea/flake.nix @@ -1,35 +1,38 @@ { description = "PoC SP module for Gitea forge service"; - outputs = { self }: { - nixosModules.default = import ./module.nix; - configPathsNeeded = - builtins.fromJSON (builtins.readFile ./config-paths-needed.json); - meta = { lib, ... }: { - spModuleSchemaVersion = 1; - id = "gitea"; - name = "Forgejo"; - description = "Forgejo is a Git forge."; - svgIcon = builtins.readFile ./icon.svg; - isMovable = true; - isRequired = false; - backupDescription = "Git repositories, database and user data."; - systemdServices = [ - "forgejo.service" - ]; - folders = [ - "/var/lib/gitea" - ]; - license = [ - lib.licenses.gpl3Plus - ]; - homepage = "https://forgejo.org"; - sourcePage = "https://codeberg.org/forgejo/forgejo"; - supportLevel = "normal"; - sso = { - userGroup = "sp.gitea.users"; - adminGroup = "sp.gitea.admins"; - }; + outputs = + { self }: + { + nixosModules.default = import ./module.nix; + configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json); + meta = + { lib, ... }: + { + spModuleSchemaVersion = 1; + id = "gitea"; + name = "Forgejo"; + description = "Forgejo is a Git forge."; + svgIcon = builtins.readFile ./icon.svg; + isMovable = true; + isRequired = false; + backupDescription = "Git repositories, database and user data."; + systemdServices = [ + "forgejo.service" + ]; + folders = [ + "/var/lib/gitea" + ]; + license = [ + lib.licenses.gpl3Plus + ]; + homepage = "https://forgejo.org"; + sourcePage = "https://codeberg.org/forgejo/forgejo"; + supportLevel = "normal"; + sso = { + userGroup = "sp.gitea.users"; + adminGroup = "sp.gitea.admins"; + }; + }; }; - }; } diff --git a/sp-modules/gitea/module.nix b/sp-modules/gitea/module.nix index 969ba47..0bf2d24 100644 --- a/sp-modules/gitea/module.nix +++ b/sp-modules/gitea/module.nix @@ -1,10 +1,12 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let sp = config.selfprivacy; - stateDir = - if sp.useBinds - then "/volumes/${cfg.location}/gitea" - else "/var/lib/gitea"; + stateDir = if sp.useBinds then "/volumes/${cfg.location}/gitea" else "/var/lib/gitea"; cfg = sp.modules.gitea; themes = [ "forgejo-auto" @@ -18,8 +20,7 @@ let oauthClientID = "forgejo"; auth-passthru = config.selfprivacy.passthru.auth; oauth2-provider-name = auth-passthru.oauth2-provider-name; - redirect-uri = - "https://${cfg.subdomain}.${sp.domain}/user/oauth2/${oauth2-provider-name}/callback"; + redirect-uri = "https://${cfg.subdomain}.${sp.domain}/user/oauth2/${oauth2-provider-name}/callback"; oauthDiscoveryURL = auth-passthru.oauth2-discovery-url oauthClientID; # SelfPrivacy uses SP Module ID to identify the group! @@ -30,381 +31,401 @@ let linuxGroupOfService = "gitea"; forgejoPackage = pkgs.forgejo; - serviceAccountTokenFP = - auth-passthru.mkServiceAccountTokenFP linuxGroupOfService; - oauthClientSecretFP = - auth-passthru.mkOAuth2ClientSecretFP linuxGroupOfService; + serviceAccountTokenFP = auth-passthru.mkServiceAccountTokenFP linuxGroupOfService; + oauthClientSecretFP = auth-passthru.mkOAuth2ClientSecretFP linuxGroupOfService; in { options.selfprivacy.modules.gitea = { - enable = (lib.mkOption { - default = false; - type = lib.types.bool; - description = "Enable Forgejo"; - }) // { - meta = { - type = "enable"; + enable = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable Forgejo"; + }) + // { + meta = { + type = "enable"; + }; }; - }; - location = (lib.mkOption { - type = lib.types.str; - description = "Forgejo location"; - }) // { - meta = { - type = "location"; + location = + (lib.mkOption { + type = lib.types.str; + description = "Forgejo location"; + }) + // { + meta = { + type = "location"; + }; }; - }; - subdomain = (lib.mkOption { - default = "git"; - 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; + subdomain = + (lib.mkOption { + default = "git"; + 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; + }; }; - }; - appName = (lib.mkOption { - default = "SelfPrivacy git Service"; - type = lib.types.str; - description = "The name displayed in the web interface"; - }) // { - meta = { - type = "string"; - weight = 1; + appName = + (lib.mkOption { + default = "SelfPrivacy git Service"; + type = lib.types.str; + description = "The name displayed in the web interface"; + }) + // { + meta = { + type = "string"; + weight = 1; + }; }; - }; - enableLfs = (lib.mkOption { - default = true; - type = lib.types.bool; - description = "Enable Git LFS"; - }) // { - meta = { - type = "bool"; - weight = 2; + enableLfs = + (lib.mkOption { + default = true; + type = lib.types.bool; + description = "Enable Git LFS"; + }) + // { + meta = { + type = "bool"; + weight = 2; + }; }; - }; - forcePrivate = (lib.mkOption { - default = false; - type = lib.types.bool; - description = "Force all new repositories to be private"; - }) // { - meta = { - type = "bool"; - weight = 3; + forcePrivate = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Force all new repositories to be private"; + }) + // { + meta = { + type = "bool"; + weight = 3; + }; }; - }; - disableRegistration = (lib.mkOption { - default = false; - type = lib.types.bool; - description = "Disable registration of new users"; - }) // { - meta = { - type = "bool"; - weight = 4; + disableRegistration = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Disable registration of new users"; + }) + // { + meta = { + type = "bool"; + weight = 4; + }; }; - }; - requireSigninView = (lib.mkOption { - default = false; - type = lib.types.bool; - description = "Force users to log in to view any page"; - }) // { - meta = { - type = "bool"; - weight = 5; + requireSigninView = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Force users to log in to view any page"; + }) + // { + meta = { + type = "bool"; + weight = 5; + }; }; - }; - defaultTheme = (lib.mkOption { - default = "forgejo-auto"; - type = lib.types.enum themes; - description = "Default theme"; - }) // { - meta = { - type = "enum"; - options = themes; - weight = 6; + defaultTheme = + (lib.mkOption { + default = "forgejo-auto"; + type = lib.types.enum themes; + description = "Default theme"; + }) + // { + meta = { + type = "enum"; + options = themes; + weight = 6; + }; }; - }; - enableSso = (lib.mkOption { - default = false; - type = lib.types.bool; - description = "Enable Single Sign-On"; - }) // { - meta = { - type = "bool"; - weight = 7; + enableSso = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable Single Sign-On"; + }) + // { + meta = { + type = "bool"; + weight = 7; + }; }; - }; - debug = (lib.mkOption { - default = false; - type = lib.types.bool; - description = "Enable debug logging"; - }) // { - meta = { - type = "bool"; - weight = 8; + debug = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable debug logging"; + }) + // { + meta = { + type = "bool"; + weight = 8; + }; }; - }; }; - config = lib.mkIf cfg.enable (lib.mkMerge [ - { - assertions = [ - { - assertion = cfg.enableSso -> sp.sso.enable; - message = - "SSO cannot be enabled for Forgejo when SSO is disabled globally."; - } - ]; - fileSystems = lib.mkIf sp.useBinds { - "/var/lib/gitea" = { - device = "/volumes/${cfg.location}/gitea"; - options = [ "bind" ]; + config = lib.mkIf cfg.enable ( + lib.mkMerge [ + { + assertions = [ + { + assertion = cfg.enableSso -> sp.sso.enable; + message = "SSO cannot be enabled for Forgejo when SSO is disabled globally."; + } + ]; + fileSystems = lib.mkIf sp.useBinds { + "/var/lib/gitea" = { + device = "/volumes/${cfg.location}/gitea"; + options = [ "bind" ]; + }; }; - }; - services.gitea.enable = false; - services.forgejo = { - enable = true; - package = forgejoPackage; - inherit stateDir; - user = linuxUserOfService; - group = linuxGroupOfService; - database = { - type = "sqlite3"; - host = "127.0.0.1"; - name = "gitea"; + services.gitea.enable = false; + services.forgejo = { + enable = true; + package = forgejoPackage; + inherit stateDir; user = linuxUserOfService; - path = "${stateDir}/data/gitea.db"; - createDatabase = true; + group = linuxGroupOfService; + database = { + type = "sqlite3"; + host = "127.0.0.1"; + name = "gitea"; + user = linuxUserOfService; + path = "${stateDir}/data/gitea.db"; + createDatabase = true; + }; + # ssh = { + # enable = true; + # clonePort = 22; + # }; + lfs = { + enable = cfg.enableLfs; + contentDir = "${stateDir}/lfs"; + }; + repositoryRoot = "${stateDir}/repositories"; + # cookieSecure = true; + settings = { + DEFAULT = { + APP_NAME = "${cfg.appName}"; + }; + server = { + DOMAIN = "${cfg.subdomain}.${sp.domain}"; + ROOT_URL = "https://${cfg.subdomain}.${sp.domain}/"; + HTTP_ADDR = "0.0.0.0"; + HTTP_PORT = 3000; + }; + mailer = { + ENABLED = false; + }; + ui = { + DEFAULT_THEME = cfg.defaultTheme; + SHOW_USER_EMAIL = false; + }; + picture = { + DISABLE_GRAVATAR = true; + }; + admin = { + ENABLE_KANBAN_BOARD = true; + }; + repository = { + FORCE_PRIVATE = cfg.forcePrivate; + }; + session = { + COOKIE_SECURE = true; + }; + log = { + ROOT_PATH = "${stateDir}/log"; + LEVEL = if cfg.debug then "Warn" else "Trace"; + }; + service = { + DISABLE_REGISTRATION = cfg.disableRegistration; + REQUIRE_SIGNIN_VIEW = cfg.requireSigninView; + }; + }; }; - # ssh = { - # enable = true; - # clonePort = 22; - # }; - lfs = { - enable = cfg.enableLfs; - contentDir = "${stateDir}/lfs"; + + users.users.gitea = { + home = "${stateDir}"; + useDefaultShell = true; + group = linuxGroupOfService; + isSystemUser = true; }; - repositoryRoot = "${stateDir}/repositories"; - # cookieSecure = true; - settings = { - DEFAULT = { - APP_NAME = "${cfg.appName}"; + users.groups.${linuxGroupOfService} = { }; + services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = { + useACMEHost = sp.domain; + forceSSL = true; + extraConfig = '' + add_header Strict-Transport-Security $hsts_header; + #add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always; + add_header 'Referrer-Policy' 'origin-when-cross-origin'; + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; + ''; + locations = { + "/" = { + proxyPass = "http://127.0.0.1:3000"; + }; }; - server = { - DOMAIN = "${cfg.subdomain}.${sp.domain}"; - ROOT_URL = "https://${cfg.subdomain}.${sp.domain}/"; - HTTP_ADDR = "0.0.0.0"; - HTTP_PORT = 3000; + }; + systemd = { + services.forgejo = { + unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/gitea"; + serviceConfig = { + Slice = "gitea.slice"; + }; }; - mailer = { - ENABLED = false; - }; - ui = { - DEFAULT_THEME = cfg.defaultTheme; - SHOW_USER_EMAIL = false; - }; - picture = { - DISABLE_GRAVATAR = true; - }; - admin = { - ENABLE_KANBAN_BOARD = true; - }; - repository = { - FORCE_PRIVATE = cfg.forcePrivate; - }; - session = { - COOKIE_SECURE = true; - }; - log = { - ROOT_PATH = "${stateDir}/log"; - LEVEL = if cfg.debug then "Warn" else "Trace"; + slices.gitea = { + description = "Forgejo service slice"; }; + }; + } + # the following part is active only when enableSso = true + (lib.mkIf is-auth-enabled { + services.forgejo.settings = { + auth.DISABLE_LOGIN_FORM = true; service = { DISABLE_REGISTRATION = cfg.disableRegistration; REQUIRE_SIGNIN_VIEW = cfg.requireSigninView; + ALLOW_ONLY_EXTERNAL_REGISTRATION = true; + SHOW_REGISTRATION_BUTTON = false; + ENABLE_BASIC_AUTHENTICATION = false; + }; + + # disallow explore page and access to private repositories, but allow public + "service.explore".REQUIRE_SIGNIN_VIEW = true; + + # TODO control via selfprivacy parameter + # "service.explore".DISABLE_USERS_PAGE = true; + + oauth2_client = { + REDIRECT_URI = redirect-uri; + ACCOUNT_LINKING = "auto"; + ENABLE_AUTO_REGISTRATION = true; + OPENID_CONNECT_SCOPES = "email openid profile"; + }; + # doesn't work if LDAP auth source is not active! + "cron.sync_external_users" = { + ENABLED = true; + RUN_AT_START = true; + NOTICE_ON_SUCCESS = true; }; }; - }; + systemd.services.forgejo = { + preStart = + let + waitForURL = url: maxRetries: delaySec: '' + for ((i=1; i<=${toString maxRetries}; i++)) + do + if ${lib.getExe pkgs.curl} -X GET --silent --fail "${url}" > /dev/null + then + echo "${url} responds to GET HTTP request (attempt #$i)" + break + else + echo "${url} does not respond to GET HTTP request (attempt #$i)" + echo sleeping for ${toString delaySec} seconds + fi + sleep ${toString delaySec} + done + if [[ "$i" -gt "${toString maxRetries}" ]] + then + echo "error, max attempts to access "${url}" have been used unsuccessfully!" + exit 124 + fi + ''; - users.users.gitea = { - home = "${stateDir}"; - useDefaultShell = true; - group = linuxGroupOfService; - isSystemUser = true; - }; - users.groups.${linuxGroupOfService} = { }; - services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = { - useACMEHost = sp.domain; - forceSSL = true; - extraConfig = '' - add_header Strict-Transport-Security $hsts_header; - #add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always; - add_header 'Referrer-Policy' 'origin-when-cross-origin'; - add_header X-Frame-Options DENY; - add_header X-Content-Type-Options nosniff; - add_header X-XSS-Protection "1; mode=block"; - proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; - ''; - locations = { - "/" = { - proxyPass = "http://127.0.0.1:3000"; + exe = lib.getExe config.services.forgejo.package; + # FIXME skip-tls-verify, bind-password + ldapConfigArgs = '' + --name LDAP \ + --active \ + --security-protocol LDAPS \ + --skip-tls-verify \ + --host '${auth-passthru.ldap-host}' \ + --port '${toString auth-passthru.ldap-port}' \ + --user-search-base '${auth-passthru.ldap-base-dn}' \ + --user-filter '(&(class=person)(memberof=${usersGroup})(name=%s))' \ + --admin-filter '(&(class=person)(memberof=${adminsGroup})' \ + --username-attribute name \ + --firstname-attribute name \ + --surname-attribute displayname \ + --email-attribute mail \ + --public-ssh-key-attribute sshPublicKey \ + --bind-dn 'dn=token' \ + --bind-password "$(< ${serviceAccountTokenFP})" \ + --synchronize-users + ''; + oauthConfigArgs = '' + --name "${oauth2-provider-name}" \ + --provider openidConnect \ + --key forgejo \ + --secret "$(< ${oauthClientSecretFP})" \ + --group-claim-name groups \ + --admin-group admins \ + --auto-discover-url '${oauthDiscoveryURL}' + ''; + in + lib.mkMerge [ + (waitForURL oauthDiscoveryURL 10 10) + (lib.mkAfter '' + set -o xtrace + + # Check if LDAP is already configured + ldap_line="$(${exe} admin auth list | grep LDAP | head -n 1)" + + if [[ -n "$ldap_line" ]]; then + # update ldap config + id="$(echo "$ldap_line" | ${pkgs.gawk}/bin/awk '{print $1}')" + ${exe} admin auth update-ldap --id "$id" ${ldapConfigArgs} + else + # initially configure ldap + ${exe} admin auth add-ldap ${ldapConfigArgs} + fi + + oauth_line="$(${exe} admin auth list | grep "${oauth2-provider-name}" | head -n 1)" + if [[ -n "$oauth_line" ]]; then + id="$(echo "$oauth_line" | ${pkgs.gawk}/bin/awk '{print $1}')" + ${exe} admin auth update-oauth --id "$id" ${oauthConfigArgs} + else + ${exe} admin auth add-oauth ${oauthConfigArgs} + fi + '') + ]; + }; + + services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = { + extraConfig = lib.mkAfter '' + rewrite ^/user/login$ /user/oauth2/${oauth2-provider-name} last; + # FIXME is it needed? + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + ''; + }; + + selfprivacy.auth.clients."${oauthClientID}" = { + inherit adminsGroup usersGroup; + imageFile = ./icon.svg; + displayName = "Forgejo"; + subdomain = cfg.subdomain; + isTokenNeeded = true; + originLanding = "https://${cfg.subdomain}.${sp.domain}/user/login?redirect_to=%2f"; + originUrl = redirect-uri; + clientSystemdUnits = [ "forgejo.service" ]; + enablePkce = lib.versionAtLeast forgejoPackage.version "8.0"; + linuxUserOfClient = linuxUserOfService; + linuxGroupOfClient = linuxGroupOfService; + claimMaps.groups = { + joinType = "array"; + valuesByGroup.${adminsGroup} = [ "admins" ]; }; }; - }; - systemd = { - services.forgejo = { - unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/gitea"; - serviceConfig = { - Slice = "gitea.slice"; - }; - }; - slices.gitea = { - description = "Forgejo service slice"; - }; - }; - } - # the following part is active only when enableSso = true - (lib.mkIf is-auth-enabled { - services.forgejo.settings = { - auth.DISABLE_LOGIN_FORM = true; - service = { - DISABLE_REGISTRATION = cfg.disableRegistration; - REQUIRE_SIGNIN_VIEW = cfg.requireSigninView; - ALLOW_ONLY_EXTERNAL_REGISTRATION = true; - SHOW_REGISTRATION_BUTTON = false; - ENABLE_BASIC_AUTHENTICATION = false; - }; - - # disallow explore page and access to private repositories, but allow public - "service.explore".REQUIRE_SIGNIN_VIEW = true; - - # TODO control via selfprivacy parameter - # "service.explore".DISABLE_USERS_PAGE = true; - - oauth2_client = { - REDIRECT_URI = redirect-uri; - ACCOUNT_LINKING = "auto"; - ENABLE_AUTO_REGISTRATION = true; - OPENID_CONNECT_SCOPES = "email openid profile"; - }; - # doesn't work if LDAP auth source is not active! - "cron.sync_external_users" = { - ENABLED = true; - RUN_AT_START = true; - NOTICE_ON_SUCCESS = true; - }; - }; - systemd.services.forgejo = { - preStart = - let - waitForURL = url: maxRetries: delaySec: '' - for ((i=1; i<=${toString maxRetries}; i++)) - do - if ${lib.getExe pkgs.curl} -X GET --silent --fail "${url}" > /dev/null - then - echo "${url} responds to GET HTTP request (attempt #$i)" - break - else - echo "${url} does not respond to GET HTTP request (attempt #$i)" - echo sleeping for ${toString delaySec} seconds - fi - sleep ${toString delaySec} - done - if [[ "$i" -gt "${toString maxRetries}" ]] - then - echo "error, max attempts to access "${url}" have been used unsuccessfully!" - exit 124 - fi - ''; - - exe = lib.getExe config.services.forgejo.package; - # FIXME skip-tls-verify, bind-password - ldapConfigArgs = '' - --name LDAP \ - --active \ - --security-protocol LDAPS \ - --skip-tls-verify \ - --host '${auth-passthru.ldap-host}' \ - --port '${toString auth-passthru.ldap-port}' \ - --user-search-base '${auth-passthru.ldap-base-dn}' \ - --user-filter '(&(class=person)(memberof=${usersGroup})(name=%s))' \ - --admin-filter '(&(class=person)(memberof=${adminsGroup})' \ - --username-attribute name \ - --firstname-attribute name \ - --surname-attribute displayname \ - --email-attribute mail \ - --public-ssh-key-attribute sshPublicKey \ - --bind-dn 'dn=token' \ - --bind-password "$(< ${serviceAccountTokenFP})" \ - --synchronize-users - ''; - oauthConfigArgs = '' - --name "${oauth2-provider-name}" \ - --provider openidConnect \ - --key forgejo \ - --secret "$(< ${oauthClientSecretFP})" \ - --group-claim-name groups \ - --admin-group admins \ - --auto-discover-url '${oauthDiscoveryURL}' - ''; - in - lib.mkMerge [ - (waitForURL oauthDiscoveryURL 10 10) - (lib.mkAfter '' - set -o xtrace - - # Check if LDAP is already configured - ldap_line="$(${exe} admin auth list | grep LDAP | head -n 1)" - - if [[ -n "$ldap_line" ]]; then - # update ldap config - id="$(echo "$ldap_line" | ${pkgs.gawk}/bin/awk '{print $1}')" - ${exe} admin auth update-ldap --id "$id" ${ldapConfigArgs} - else - # initially configure ldap - ${exe} admin auth add-ldap ${ldapConfigArgs} - fi - - oauth_line="$(${exe} admin auth list | grep "${oauth2-provider-name}" | head -n 1)" - if [[ -n "$oauth_line" ]]; then - id="$(echo "$oauth_line" | ${pkgs.gawk}/bin/awk '{print $1}')" - ${exe} admin auth update-oauth --id "$id" ${oauthConfigArgs} - else - ${exe} admin auth add-oauth ${oauthConfigArgs} - fi - '') - ]; - }; - - services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = { - extraConfig = lib.mkAfter '' - rewrite ^/user/login$ /user/oauth2/${oauth2-provider-name} last; - # FIXME is it needed? - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - ''; - }; - - selfprivacy.auth.clients."${oauthClientID}" = { - inherit adminsGroup usersGroup; - imageFile = ./icon.svg; - displayName = "Forgejo"; - subdomain = cfg.subdomain; - isTokenNeeded = true; - originLanding = - "https://${cfg.subdomain}.${sp.domain}/user/login?redirect_to=%2f"; - originUrl = redirect-uri; - clientSystemdUnits = [ "forgejo.service" ]; - enablePkce = lib.versionAtLeast forgejoPackage.version "8.0"; - linuxUserOfClient = linuxUserOfService; - linuxGroupOfClient = linuxGroupOfService; - claimMaps.groups = { - joinType = "array"; - valuesByGroup.${adminsGroup} = [ "admins" ]; - }; - }; - }) - ]); + }) + ] + ); } diff --git a/sp-modules/jitsi-meet/flake.nix b/sp-modules/jitsi-meet/flake.nix index 3cdee32..d4c7d76 100644 --- a/sp-modules/jitsi-meet/flake.nix +++ b/sp-modules/jitsi-meet/flake.nix @@ -1,33 +1,36 @@ { description = "PoC SP module for Jitsi Meet video conferences server"; - outputs = { self }: { - nixosModules.default = import ./module.nix; - configPathsNeeded = - builtins.fromJSON (builtins.readFile ./config-paths-needed.json); - meta = { lib, ... }: { - spModuleSchemaVersion = 1; - id = "jitsi-meet"; - name = "JitsiMeet"; - description = "Jitsi Meet is a free and open-source video conferencing solution."; - svgIcon = builtins.readFile ./icon.svg; - isMovable = false; - isRequired = false; - backupDescription = "Secrets that are used to encrypt the communication."; - systemdServices = [ - "prosody.service" - "jitsi-videobridge2.service" - "jicofo.service" - ]; - folders = [ - "/var/lib/jitsi-meet" - ]; - license = [ - lib.licenses.asl20 - ]; - homepage = "https://jitsi.org/meet"; - sourcePage = "https://github.com/jitsi/jitsi-meet"; - supportLevel = "normal"; + outputs = + { self }: + { + nixosModules.default = import ./module.nix; + configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json); + meta = + { lib, ... }: + { + spModuleSchemaVersion = 1; + id = "jitsi-meet"; + name = "JitsiMeet"; + description = "Jitsi Meet is a free and open-source video conferencing solution."; + svgIcon = builtins.readFile ./icon.svg; + isMovable = false; + isRequired = false; + backupDescription = "Secrets that are used to encrypt the communication."; + systemdServices = [ + "prosody.service" + "jitsi-videobridge2.service" + "jicofo.service" + ]; + folders = [ + "/var/lib/jitsi-meet" + ]; + license = [ + lib.licenses.asl20 + ]; + homepage = "https://jitsi.org/meet"; + sourcePage = "https://github.com/jitsi/jitsi-meet"; + supportLevel = "normal"; + }; }; - }; } diff --git a/sp-modules/jitsi-meet/module.nix b/sp-modules/jitsi-meet/module.nix index 96c3ff6..6f9a5bb 100644 --- a/sp-modules/jitsi-meet/module.nix +++ b/sp-modules/jitsi-meet/module.nix @@ -5,37 +5,43 @@ let in { options.selfprivacy.modules.jitsi-meet = { - enable = (lib.mkOption { - default = false; - type = lib.types.bool; - description = "Enable JitsiMeet"; - }) // { - meta = { - type = "enable"; + enable = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable JitsiMeet"; + }) + // { + meta = { + type = "enable"; + }; }; - }; - subdomain = (lib.mkOption { - default = "meet"; - 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; + subdomain = + (lib.mkOption { + default = "meet"; + 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; + }; }; - }; - appName = (lib.mkOption { - default = "Jitsi Meet"; - type = lib.types.str; - description = "The name displayed in the web interface"; - }) // { - meta = { - type = "string"; - weight = 1; + appName = + (lib.mkOption { + default = "Jitsi Meet"; + type = lib.types.str; + description = "The name displayed in the web interface"; + }) + // { + meta = { + type = "string"; + weight = 1; + }; }; - }; }; config = lib.mkIf cfg.enable { @@ -43,7 +49,9 @@ in (_: prev: { # We disable E2E for clients below jitsi-meet = prev.jitsi-meet.overrideAttrs (old: { - meta = old.meta // { knownVulnerabilities = [ ]; }; + meta = old.meta // { + knownVulnerabilities = [ ]; + }; }); }) ]; diff --git a/sp-modules/monitoring/flake.nix b/sp-modules/monitoring/flake.nix index e71fa13..e4592a5 100644 --- a/sp-modules/monitoring/flake.nix +++ b/sp-modules/monitoring/flake.nix @@ -1,36 +1,39 @@ { description = "PoC SP module for Prometheus-based monitoring"; - outputs = { self }: { - nixosModules.default = import ./module.nix; - configPathsNeeded = - builtins.fromJSON (builtins.readFile ./config-paths-needed.json); - meta = { lib, ... }: { - spModuleSchemaVersion = 1; - id = "monitoring"; - name = "Prometheus"; - description = "Prometheus is used for resource monitoring and alerts."; - svgIcon = builtins.readFile ./icon.svg; - isMovable = false; - isRequired = true; - canBeBackedUp = false; - backupDescription = "Backups are not available for Prometheus."; - systemdServices = [ - "prometheus.service" - ]; - ownedFolders = [ + outputs = + { self }: + { + nixosModules.default = import ./module.nix; + configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json); + meta = + { lib, ... }: { - path = "/var/lib/prometheus"; - owner = "prometheus"; - group = "prometheus"; - } - ]; - license = [ - lib.licenses.asl20 - ]; - homepage = "https://prometheus.io/"; - sourcePage = "https://prometheus.io/"; - supportLevel = "normal"; + spModuleSchemaVersion = 1; + id = "monitoring"; + name = "Prometheus"; + description = "Prometheus is used for resource monitoring and alerts."; + svgIcon = builtins.readFile ./icon.svg; + isMovable = false; + isRequired = true; + canBeBackedUp = false; + backupDescription = "Backups are not available for Prometheus."; + systemdServices = [ + "prometheus.service" + ]; + ownedFolders = [ + { + path = "/var/lib/prometheus"; + owner = "prometheus"; + group = "prometheus"; + } + ]; + license = [ + lib.licenses.asl20 + ]; + homepage = "https://prometheus.io/"; + sourcePage = "https://prometheus.io/"; + supportLevel = "normal"; + }; }; - }; } diff --git a/sp-modules/monitoring/module.nix b/sp-modules/monitoring/module.nix index 9b2f92c..4db9b55 100644 --- a/sp-modules/monitoring/module.nix +++ b/sp-modules/monitoring/module.nix @@ -4,23 +4,27 @@ let in { options.selfprivacy.modules.monitoring = { - enable = (lib.mkOption { - default = false; - type = lib.types.bool; - description = "Enable monitoring service"; - }) // { - meta = { - type = "enable"; + enable = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable monitoring service"; + }) + // { + meta = { + type = "enable"; + }; }; - }; - location = (lib.mkOption { - type = lib.types.str; - description = "Monitoring data location"; - }) // { - meta = { - type = "location"; + location = + (lib.mkOption { + type = lib.types.str; + description = "Monitoring data location"; + }) + // { + meta = { + type = "location"; + }; }; - }; }; config = lib.mkIf cfg.enable { fileSystems = lib.mkIf config.selfprivacy.useBinds { @@ -54,15 +58,19 @@ in scrapeConfigs = [ { job_name = "node-exporter"; - static_configs = [{ - targets = [ "127.0.0.1:9002" ]; - }]; + static_configs = [ + { + targets = [ "127.0.0.1:9002" ]; + } + ]; } { job_name = "cadvisor"; - static_configs = [{ - targets = [ "127.0.0.1:9003" ]; - }]; + static_configs = [ + { + targets = [ "127.0.0.1:9003" ]; + } + ]; } ]; }; diff --git a/sp-modules/mumble/flake.nix b/sp-modules/mumble/flake.nix index 6512648..17d8dbc 100644 --- a/sp-modules/mumble/flake.nix +++ b/sp-modules/mumble/flake.nix @@ -1,35 +1,38 @@ { description = "PoC SP module for Mumble conferences server"; - outputs = { self }: { - nixosModules.default = import ./module.nix; - configPathsNeeded = - builtins.fromJSON (builtins.readFile ./config-paths-needed.json); - meta = { lib, ... }: { - spModuleSchemaVersion = 1; - id = "mumble"; - name = "Mumble"; - description = "Open Source, Low Latency, High Quality Voice Chat."; - svgIcon = builtins.readFile ./icon.svg; - showUrl = false; - isMovable = true; - isRequired = false; - canBeBackedUp = true; - backupDescription = "Mumble server data."; - systemdServices = [ - "murmur.service" - ]; - user = "murmur"; - group = "murmur"; - folders = [ - "/var/lib/murmur" - ]; - license = [ - lib.licenses.bsd3 - ]; - homepage = "https://www.mumble.info"; - sourcePage = "https://github.com/mumble-voip/mumble"; - supportLevel = "normal"; + outputs = + { self }: + { + nixosModules.default = import ./module.nix; + configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json); + meta = + { lib, ... }: + { + spModuleSchemaVersion = 1; + id = "mumble"; + name = "Mumble"; + description = "Open Source, Low Latency, High Quality Voice Chat."; + svgIcon = builtins.readFile ./icon.svg; + showUrl = false; + isMovable = true; + isRequired = false; + canBeBackedUp = true; + backupDescription = "Mumble server data."; + systemdServices = [ + "murmur.service" + ]; + user = "murmur"; + group = "murmur"; + folders = [ + "/var/lib/murmur" + ]; + license = [ + lib.licenses.bsd3 + ]; + homepage = "https://www.mumble.info"; + sourcePage = "https://github.com/mumble-voip/mumble"; + supportLevel = "normal"; + }; }; - }; } diff --git a/sp-modules/mumble/module.nix b/sp-modules/mumble/module.nix index 0e744b9..e7eb8ef 100644 --- a/sp-modules/mumble/module.nix +++ b/sp-modules/mumble/module.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let domain = config.selfprivacy.domain; sp = config.selfprivacy; @@ -6,55 +11,65 @@ let in { options.selfprivacy.modules.mumble = { - enable = (lib.mkOption { - default = false; - type = lib.types.bool; - description = "Enable Mumble"; - }) // { - meta = { - type = "enable"; + enable = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable Mumble"; + }) + // { + meta = { + type = "enable"; + }; }; - }; - subdomain = (lib.mkOption { - default = "mumble"; - 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; + subdomain = + (lib.mkOption { + default = "mumble"; + 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; + }; }; - }; - location = (lib.mkOption { - type = lib.types.str; - description = "Location"; - }) // { - meta = { - type = "location"; + location = + (lib.mkOption { + type = lib.types.str; + description = "Location"; + }) + // { + meta = { + type = "location"; + }; }; - }; - appName = (lib.mkOption { - default = "SelfPrivacy Mumble Service"; - type = lib.types.str; - description = "The name of your Mumble server"; - }) // { - meta = { - type = "string"; - weight = 1; + appName = + (lib.mkOption { + default = "SelfPrivacy Mumble Service"; + type = lib.types.str; + description = "The name of your Mumble server"; + }) + // { + meta = { + type = "string"; + weight = 1; + }; }; - }; - welcomeText = (lib.mkOption { - default = "Welcome to my Mumble server!"; - type = lib.types.str; - description = "Welcome message"; - }) // { - meta = { - type = "string"; - weight = 2; + welcomeText = + (lib.mkOption { + default = "Welcome to my Mumble server!"; + type = lib.types.str; + description = "Welcome message"; + }) + // { + meta = { + type = "string"; + weight = 2; + }; }; - }; }; config = lib.mkIf cfg.enable { @@ -79,7 +94,9 @@ in }; systemd = { services = { - murmur = { serviceConfig.Slice = "mumble.slice"; }; + murmur = { + serviceConfig.Slice = "mumble.slice"; + }; murmur-ensure-folder-ownership = { description = "Ensure murmur folder ownership"; before = [ "murmur.service" ]; diff --git a/sp-modules/nextcloud/cleanup-module.nix b/sp-modules/nextcloud/cleanup-module.nix index 0b3b3ae..b146ee5 100644 --- a/sp-modules/nextcloud/cleanup-module.nix +++ b/sp-modules/nextcloud/cleanup-module.nix @@ -13,8 +13,8 @@ in system.activationScripts.nextcloudSecrets = lib.trivial.warn ( - "nextcloud service is disabled, " + - "${override-config-fp}, ${db-pass-filepath} and ${admin-pass-filepath} will be removed!" + "nextcloud service is disabled, " + + "${override-config-fp}, ${db-pass-filepath} and ${admin-pass-filepath} will be removed!" ) '' rm -f -v ${db-pass-filepath} diff --git a/sp-modules/nextcloud/common.nix b/sp-modules/nextcloud/common.nix index 3fb4709..453ee18 100644 --- a/sp-modules/nextcloud/common.nix +++ b/sp-modules/nextcloud/common.nix @@ -1,6 +1,6 @@ config: rec { sp = config.selfprivacy; - domain= sp.domain; + domain = sp.domain; secrets-filepath = "/etc/selfprivacy/secrets.json"; db-pass-filepath = "/var/lib/nextcloud/db-pass"; admin-pass-filepath = "/var/lib/nextcloud/admin-pass"; diff --git a/sp-modules/nextcloud/flake.nix b/sp-modules/nextcloud/flake.nix index b6deec9..25083af 100644 --- a/sp-modules/nextcloud/flake.nix +++ b/sp-modules/nextcloud/flake.nix @@ -1,38 +1,45 @@ { description = "PoC SP module for nextcloud"; - outputs = { self }: { - nixosModules.default = _: - { imports = [ ./module.nix ./cleanup-module.nix ]; }; - configPathsNeeded = - builtins.fromJSON (builtins.readFile ./config-paths-needed.json); - meta = { lib, ... }: { - spModuleSchemaVersion = 1; - id = "nextcloud"; - name = "Nextcloud"; - description = "Nextcloud is a cloud storage service that offers a web interface and a desktop client."; - svgIcon = builtins.readFile ./icon.svg; - isMovable = true; - isRequired = false; - canBeBackedUp = true; - backupDescription = "All the files and other data stored in Nextcloud."; - systemdServices = [ - "phpfpm-nextcloud.service" - "redis-nextcloud.service" - ]; - folders = [ - "/var/lib/nextcloud" - ]; - license = [ - lib.licenses.agpl3Plus - ]; - homepage = "https://nextcloud.com/"; - sourcePage = "https://github.com/nextcloud"; - supportLevel = "normal"; - sso = { - userGroup = "sp.nextcloud.users"; - adminGroup = "sp.nextcloud.admins"; + outputs = + { self }: + { + nixosModules.default = _: { + imports = [ + ./module.nix + ./cleanup-module.nix + ]; }; + configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json); + meta = + { lib, ... }: + { + spModuleSchemaVersion = 1; + id = "nextcloud"; + name = "Nextcloud"; + description = "Nextcloud is a cloud storage service that offers a web interface and a desktop client."; + svgIcon = builtins.readFile ./icon.svg; + isMovable = true; + isRequired = false; + canBeBackedUp = true; + backupDescription = "All the files and other data stored in Nextcloud."; + systemdServices = [ + "phpfpm-nextcloud.service" + "redis-nextcloud.service" + ]; + folders = [ + "/var/lib/nextcloud" + ]; + license = [ + lib.licenses.agpl3Plus + ]; + homepage = "https://nextcloud.com/"; + sourcePage = "https://github.com/nextcloud"; + supportLevel = "normal"; + sso = { + userGroup = "sp.nextcloud.users"; + adminGroup = "sp.nextcloud.admins"; + }; + }; }; - }; } diff --git a/sp-modules/nextcloud/module.nix b/sp-modules/nextcloud/module.nix index 2ac3b1a..5a18968 100644 --- a/sp-modules/nextcloud/module.nix +++ b/sp-modules/nextcloud/module.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let inherit (import ./common.nix config) admin-pass-filepath @@ -27,391 +32,417 @@ let usersGroup = "sp.${oauthClientID}.users"; wildcardGroup = "sp.${oauthClientID}.*"; - serviceAccountTokenFP = - auth-passthru.mkServiceAccountTokenFP linuxUserOfService; - oauthClientSecretFP = - auth-passthru.mkOAuth2ClientSecretFP linuxUserOfService; + serviceAccountTokenFP = auth-passthru.mkServiceAccountTokenFP linuxUserOfService; + oauthClientSecretFP = auth-passthru.mkOAuth2ClientSecretFP linuxUserOfService; - updater-page-substitute = - pkgs.runCommandNoCC "nextcloud-updater-page-substitute" { } '' - install -m644 ${./updater.html} -DT $out/index.html - ''; + updater-page-substitute = pkgs.runCommandNoCC "nextcloud-updater-page-substitute" { } '' + install -m644 ${./updater.html} -DT $out/index.html + ''; in { options.selfprivacy.modules.nextcloud = with lib; { - enable = (lib.mkOption { - default = false; - type = lib.types.bool; - description = "Enable Nextcloud"; - }) // { - meta = { - type = "enable"; + enable = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable Nextcloud"; + }) + // { + meta = { + type = "enable"; + }; }; - }; - location = (lib.mkOption { - type = lib.types.str; - description = "Nextcloud location"; - }) // { - meta = { - type = "location"; + location = + (lib.mkOption { + type = lib.types.str; + description = "Nextcloud location"; + }) + // { + meta = { + type = "location"; + }; }; - }; - subdomain = (lib.mkOption { - default = "cloud"; - 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; + subdomain = + (lib.mkOption { + default = "cloud"; + 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; + }; }; - }; - enableImagemagick = (lib.mkOption { - type = types.bool; - default = true; - description = "Enable ImageMagick"; - }) // { - meta = { - type = "bool"; - weight = 1; + enableImagemagick = + (lib.mkOption { + type = types.bool; + default = true; + description = "Enable ImageMagick"; + }) + // { + meta = { + type = "bool"; + weight = 1; + }; }; - }; - enableSso = (lib.mkOption { - default = false; - type = lib.types.bool; - description = "Enable Single Sign-On"; - }) // { - meta = { - type = "bool"; - weight = 2; + enableSso = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable Single Sign-On"; + }) + // { + meta = { + type = "bool"; + weight = 2; + }; }; - }; - enableSambaFeatures = (lib.mkOption { - type = types.bool; - default = false; - description = "Enable support for Samba/CIFS features"; - }) // { - meta = { - type = "bool"; - weight = 3; + enableSambaFeatures = + (lib.mkOption { + type = types.bool; + default = false; + description = "Enable support for Samba/CIFS features"; + }) + // { + meta = { + type = "bool"; + weight = 3; + }; }; - }; - debug = (lib.mkOption { - default = false; - type = lib.types.bool; - description = "Enable debug logging"; - }) // { - meta = { - type = "bool"; - weight = 4; + debug = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable debug logging"; + }) + // { + meta = { + type = "bool"; + weight = 4; + }; }; - }; - disableMaintenanceModeAtStart = (lib.mkOption { - type = types.bool; - default = false; - description = "Disable maintenance mode at Nextcloud service startup"; - }) // { - meta = { - type = "bool"; - weight = 5; + disableMaintenanceModeAtStart = + (lib.mkOption { + type = types.bool; + default = false; + description = "Disable maintenance mode at Nextcloud service startup"; + }) + // { + meta = { + type = "bool"; + weight = 5; + }; }; - }; }; # config = lib.mkIf sp.modules.nextcloud.enable - config = lib.mkIf sp.modules.nextcloud.enable (lib.mkMerge [ - { - assertions = [ - { - assertion = cfg.enableSso -> sp.sso.enable; - message = - "SSO cannot be enabled for Nextcloud when SSO is disabled globally."; - } - ]; - fileSystems = lib.mkIf sp.useBinds { - "/var/lib/nextcloud" = { - device = "/volumes/${cfg.location}/nextcloud"; - options = [ - "bind" - "x-systemd.required-by=nextcloud-setup.service" - "x-systemd.required-by=nextcloud-secrets.service" - "x-systemd.before=nextcloud-setup.service" - "x-systemd.before=nextcloud-secrets.service" - ]; - }; - }; - - # for ExecStartPost script to have access to /run/keys/* - users.groups.keys.members = - lib.mkIf is-auth-enabled [ linuxUserOfService ]; - - # not needed, due to turnOffCertCheck=1 in used_ldap - # users.groups.${config.security.acme.certs.${domain}.group}.members = - # [ config.services.phpfpm.pools.nextcloud.user ]; - - systemd = { - services = { - phpfpm-nextcloud.serviceConfig.Slice = lib.mkForce "nextcloud.slice"; - nextcloud-setup = { - serviceConfig.Slice = "nextcloud.slice"; - serviceConfig.Group = config.services.phpfpm.pools.nextcloud.group; + config = lib.mkIf sp.modules.nextcloud.enable ( + lib.mkMerge [ + { + assertions = [ + { + assertion = cfg.enableSso -> sp.sso.enable; + message = "SSO cannot be enabled for Nextcloud when SSO is disabled globally."; + } + ]; + fileSystems = lib.mkIf sp.useBinds { + "/var/lib/nextcloud" = { + device = "/volumes/${cfg.location}/nextcloud"; + options = [ + "bind" + "x-systemd.required-by=nextcloud-setup.service" + "x-systemd.required-by=nextcloud-secrets.service" + "x-systemd.before=nextcloud-setup.service" + "x-systemd.before=nextcloud-secrets.service" + ]; }; - nextcloud-cron.serviceConfig.Slice = "nextcloud.slice"; - nextcloud-update-db.serviceConfig.Slice = "nextcloud.slice"; - nextcloud-update-plugins.serviceConfig.Slice = "nextcloud.slice"; - nextcloud-secrets = { - before = [ "nextcloud-setup.service" ]; - requiredBy = [ "nextcloud-setup.service" ]; - serviceConfig.Type = "oneshot"; - path = with pkgs; [ coreutils jq ]; - script = '' - databasePassword=$(jq -re '.modules.nextcloud.databasePassword' ${secrets-filepath}) - adminPassword=$(jq -re '.modules.nextcloud.adminPassword' ${secrets-filepath}) + }; - install -C -m 0440 -o nextcloud -g nextcloud -DT \ - <(printf "%s\n" "$databasePassword") \ - ${db-pass-filepath} + # for ExecStartPost script to have access to /run/keys/* + users.groups.keys.members = lib.mkIf is-auth-enabled [ linuxUserOfService ]; - install -C -m 0440 -o nextcloud -g nextcloud -DT \ - <(printf "%s\n" "$adminPassword") \ - ${admin-pass-filepath} + # not needed, due to turnOffCertCheck=1 in used_ldap + # users.groups.${config.security.acme.certs.${domain}.group}.members = + # [ config.services.phpfpm.pools.nextcloud.user ]; + + systemd = { + services = { + phpfpm-nextcloud.serviceConfig.Slice = lib.mkForce "nextcloud.slice"; + nextcloud-setup = { + serviceConfig.Slice = "nextcloud.slice"; + serviceConfig.Group = config.services.phpfpm.pools.nextcloud.group; + }; + nextcloud-cron.serviceConfig.Slice = "nextcloud.slice"; + nextcloud-update-db.serviceConfig.Slice = "nextcloud.slice"; + nextcloud-update-plugins.serviceConfig.Slice = "nextcloud.slice"; + nextcloud-secrets = { + before = [ "nextcloud-setup.service" ]; + requiredBy = [ "nextcloud-setup.service" ]; + serviceConfig.Type = "oneshot"; + path = with pkgs; [ + coreutils + jq + ]; + script = '' + databasePassword=$(jq -re '.modules.nextcloud.databasePassword' ${secrets-filepath}) + adminPassword=$(jq -re '.modules.nextcloud.adminPassword' ${secrets-filepath}) + + install -C -m 0440 -o nextcloud -g nextcloud -DT \ + <(printf "%s\n" "$databasePassword") \ + ${db-pass-filepath} + + install -C -m 0440 -o nextcloud -g nextcloud -DT \ + <(printf "%s\n" "$adminPassword") \ + ${admin-pass-filepath} + ''; + }; + }; + slices.nextcloud = { + description = "Nextcloud service slice"; + }; + }; + services.nextcloud = { + enable = true; + package = pkgs.nextcloud30; + inherit hostName; + + # Use HTTPS for links + https = true; + + # auto-update Nextcloud Apps + autoUpdateApps.enable = true; + # set what time makes sense for you + autoUpdateApps.startAt = "05:00:00"; + + phpOptions.display_errors = "Off"; + phpOptions."opcache.interned_strings_buffer" = "32"; + + configureRedis = true; + + settings = + { + # further forces Nextcloud to use HTTPS + overwriteprotocol = "https"; + } + // lib.attrsets.optionalAttrs is-auth-enabled { + loglevel = 0; + # log_type = "file"; + social_login_auto_redirect = false; + + allow_local_remote_servers = true; + allow_user_to_change_display_name = false; + lost_password_link = "disabled"; + allow_multiple_user_backends = false; + + updatechecker = false; # nixpkgs handles updates for us, update via web ui will fail on nixos. + + user_oidc = { + single_logout = true; + use_pkce = true; + auto_provision = true; + soft_auto_provision = true; + disable_account_creation = false; + }; + }; + + config = { + dbtype = "sqlite"; + dbuser = "nextcloud"; + dbname = "nextcloud"; + dbpassFile = db-pass-filepath; + # TODO review whether admin user is needed at all - admin group works + adminpassFile = admin-pass-filepath; + adminuser = "admin"; + }; + }; + services.nginx.virtualHosts.${hostName} = { + useACMEHost = sp.domain; + forceSSL = true; + #locations."/".extraConfig = lib.mkIf is-auth-enabled '' + # # FIXME does not work + # rewrite ^/login$ /apps/user_oidc/login/1 last; + #''; + # show an error instead of a blank page on Nextcloud PHP/FastCGI error + locations."~ \\.php(?:$|/)".extraConfig = '' + error_page 500 502 503 504 ${pkgs.nginx}/html/50x.html; + ''; + locations."^~ /updater/" = { + alias = updater-page-substitute + "/"; + extraConfig = '' + error_page 410 /index.html; + # otherwise, nginx returns 405 for POST requests to static content + error_page 405 =200 $uri; ''; }; }; - slices.nextcloud = { - description = "Nextcloud service slice"; + } + # enables samba features when requested + (lib.mkIf cfg.enableSambaFeatures { + # only apply cifs-utils package to this module + services.phpfpm.pools.nextcloud.phpEnv.PATH = + lib.mkForce "${pkgs.samba}/bin:${pkgs.cifs-utils}/bin:/run/wrappers/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin:/usr/bin:/bin"; + systemd.services.nextcloud-cron.path = [ + pkgs.samba + pkgs.cifs-utils + ]; + }) + # the following part is active only when "auth" module is enabled + (lib.mkIf is-auth-enabled { + systemd.services.nextcloud-setup = { + serviceConfig = { + Restart = "on-failure"; + RestartSec = "60"; + }; + path = [ pkgs.jq ]; + script = lib.mkMerge [ + (lib.strings.optionalString cfg.disableMaintenanceModeAtStart ( + lib.mkBefore "${occ} maintenance:mode --no-interaction --off" + )) + '' + set -o errexit + set -o nounset + ${lib.strings.optionalString cfg.debug "set -o xtrace"} + + ${occ} app:disable logreader + + ${occ} app:install user_ldap || : + ${occ} app:enable user_ldap + + # The following code tries to match an existing config or creates a new one. + # The criteria for matching is the ldapHost value. + + # remove broken link after previous nextcloud (un)installation + [[ ! -f "${override-config-fp}" && -L "${override-config-fp}" ]] && \ + rm -v "${override-config-fp}" + + ALL_CONFIG="$(${occ} ldap:show-config --output=json)" + + MATCHING_CONFIG_IDs="$(jq '[to_entries[] | select(.value.ldapHost=="${ldap_scheme_and_host}") | .key]' <<<"$ALL_CONFIG")" + if [[ $(jq 'length' <<<"$MATCHING_CONFIG_IDs") > 0 ]]; then + CONFIG_ID="$(jq --raw-output '.[0]' <<<"$MATCHING_CONFIG_IDs")" + else + CONFIG_ID="$(${occ} ldap:create-empty-config --only-print-prefix)" + fi + + echo "Using configId $CONFIG_ID" + + # The following CLI commands follow + # https://github.com/lldap/lldap/blob/main/example_configs/nextcloud.md#nextcloud-config--the-cli-way + + # StartTLS is not supported in Kanidm due to security risks, whereas + # user_ldap doesn't support SASL. Importing certificate doesn't + # help: + # ${occ} security:certificates:import "${config.security.acme.certs.${domain}.directory}/cert.pem" + ${occ} ldap:set-config "$CONFIG_ID" 'turnOffCertCheck' '1' + + ${occ} ldap:set-config "$CONFIG_ID" 'ldapHost' '${ldap_scheme_and_host}' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapPort' '${toString auth-passthru.ldap-port}' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentName' 'dn=token' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentPassword' "$(<${serviceAccountTokenFP})" + ${occ} ldap:set-config "$CONFIG_ID" 'ldapBase' '${auth-passthru.ldap-base-dn}' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapBaseGroups' '${auth-passthru.ldap-base-dn}' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapBaseUsers' '${auth-passthru.ldap-base-dn}' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapEmailAttribute' 'mail' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilter' \ + '(&(class=group)(${wildcardGroup})' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterGroups' \ + '(&(class=group)(${wildcardGroup}))' + # ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterObjectclass' \ + # 'groupOfUniqueNames' + # ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupMemberAssocAttr' \ + # 'uniqueMember' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapLoginFilter' \ + '(&(class=person)(memberof=${usersGroup})(uid=%uid))' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapLoginFilterAttributes' \ + 'uid' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserDisplayName' \ + 'displayname' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilter' \ + '(&(class=person)(memberof=${usersGroup})(name=%s))' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterMode' \ + '1' + ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterObjectclass' \ + 'person' + + ${occ} ldap:test-config -- "$CONFIG_ID" + + # delete all configs except "$CONFIG_ID" + for configid in $(jq --raw-output "keys[] | select(. != \"$CONFIG_ID\")" <<<"$ALL_CONFIG"); do + echo "Deactivating $configid" + ${occ} ldap:set-config "$configid" 'ldapConfigurationActive' '0' + echo "Deactivated $configid" + echo "Deleting $configid" + ${occ} ldap:delete-config "$configid" + echo "Deleted $configid" + done + + ${occ} ldap:set-config "$CONFIG_ID" 'ldapConfigurationActive' '1' + + ############################################################################ + # OIDC app + ############################################################################ + ${occ} app:install user_oidc || : + ${occ} app:enable user_oidc + + ${occ} user_oidc:provider ${auth-passthru.oauth2-provider-name} \ + --clientid="${oauthClientID}" \ + --clientsecret="$(<${oauthClientSecretFP})" \ + --discoveryuri="${auth-passthru.oauth2-discovery-url "nextcloud"}" \ + --unique-uid=0 \ + --scope="email openid profile" \ + --mapping-uid=preferred_username \ + --no-interaction \ + --mapping-groups=groups \ + --group-provisioning=1 \ + -vvv + + '' + (lib.optionalString deleteNextcloudAdmin '' + if [[ ! -f /var/lib/nextcloud/.admin-user-deleted ]]; then + ${occ} user:delete admin + touch /var/lib/nextcloud/.admin-user-deleted + fi + '') + ]; }; - }; - services.nextcloud = { - enable = true; - package = pkgs.nextcloud30; - inherit hostName; - - # Use HTTPS for links - https = true; - - # auto-update Nextcloud Apps - autoUpdateApps.enable = true; - # set what time makes sense for you - autoUpdateApps.startAt = "05:00:00"; - - phpOptions.display_errors = "Off"; - phpOptions."opcache.interned_strings_buffer" = "32"; - - configureRedis = true; - - settings = { - # further forces Nextcloud to use HTTPS - overwriteprotocol = "https"; - } // lib.attrsets.optionalAttrs is-auth-enabled { - loglevel = 0; - # log_type = "file"; - social_login_auto_redirect = false; - - allow_local_remote_servers = true; - allow_user_to_change_display_name = false; - lost_password_link = "disabled"; - allow_multiple_user_backends = false; - - updatechecker = false; # nixpkgs handles updates for us, update via web ui will fail on nixos. - - user_oidc = { - single_logout = true; - use_pkce = true; - auto_provision = true; - soft_auto_provision = true; - disable_account_creation = false; + selfprivacy.auth.clients."${oauthClientID}" = { + inherit adminsGroup usersGroup; + imageFile = ./icon.svg; + displayName = "Nextcloud"; + subdomain = cfg.subdomain; + isTokenNeeded = true; + originUrl = "https://${cfg.subdomain}.${domain}/apps/user_oidc/code"; + originLanding = "https://${cfg.subdomain}.${domain}/apps/user_oidc/login/1"; + useShortPreferredUsername = true; + clientSystemdUnits = [ + "nextcloud-setup.service" + "phpfpm-nextcloud.service" + ]; + enablePkce = true; + linuxUserOfClient = linuxUserOfService; + linuxGroupOfClient = linuxGroupOfService; + scopeMaps.${usersGroup} = [ + "email" + "openid" + "profile" + ]; + claimMaps.groups = { + joinType = "array"; + valuesByGroup.${adminsGroup} = [ "admin" ]; }; }; - - config = { - dbtype = "sqlite"; - dbuser = "nextcloud"; - dbname = "nextcloud"; - dbpassFile = db-pass-filepath; - # TODO review whether admin user is needed at all - admin group works - adminpassFile = admin-pass-filepath; - adminuser = "admin"; - }; - }; - services.nginx.virtualHosts.${hostName} = { - useACMEHost = sp.domain; - forceSSL = true; - #locations."/".extraConfig = lib.mkIf is-auth-enabled '' - # # FIXME does not work - # rewrite ^/login$ /apps/user_oidc/login/1 last; - #''; - # show an error instead of a blank page on Nextcloud PHP/FastCGI error - locations."~ \\.php(?:$|/)".extraConfig = '' - error_page 500 502 503 504 ${pkgs.nginx}/html/50x.html; - ''; - locations."^~ /updater/" = { - alias = updater-page-substitute + "/"; - extraConfig = '' - error_page 410 /index.html; - # otherwise, nginx returns 405 for POST requests to static content - error_page 405 =200 $uri; + }) + (lib.mkIf (!is-auth-enabled) { + systemd.services.nextcloud-setup = { + script = '' + ${occ} app:disable logreader + ${occ} app:disable user_oidc ''; }; - }; - } - # enables samba features when requested - (lib.mkIf cfg.enableSambaFeatures { - # only apply cifs-utils package to this module - services.phpfpm.pools.nextcloud.phpEnv.PATH = - lib.mkForce "${pkgs.samba}/bin:${pkgs.cifs-utils}/bin:/run/wrappers/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin:/usr/bin:/bin"; - systemd.services.nextcloud-cron.path = [ pkgs.samba pkgs.cifs-utils ]; - }) - # the following part is active only when "auth" module is enabled - (lib.mkIf is-auth-enabled { - systemd.services.nextcloud-setup = { - serviceConfig = { - Restart = "on-failure"; - RestartSec = "60"; - }; - path = [ pkgs.jq ]; - script = lib.mkMerge [ - (lib.strings.optionalString cfg.disableMaintenanceModeAtStart ( - lib.mkBefore "${occ} maintenance:mode --no-interaction --off" - )) - '' - set -o errexit - set -o nounset - ${lib.strings.optionalString cfg.debug "set -o xtrace"} - - ${occ} app:disable logreader - - ${occ} app:install user_ldap || : - ${occ} app:enable user_ldap - - # The following code tries to match an existing config or creates a new one. - # The criteria for matching is the ldapHost value. - - # remove broken link after previous nextcloud (un)installation - [[ ! -f "${override-config-fp}" && -L "${override-config-fp}" ]] && \ - rm -v "${override-config-fp}" - - ALL_CONFIG="$(${occ} ldap:show-config --output=json)" - - MATCHING_CONFIG_IDs="$(jq '[to_entries[] | select(.value.ldapHost=="${ldap_scheme_and_host}") | .key]' <<<"$ALL_CONFIG")" - if [[ $(jq 'length' <<<"$MATCHING_CONFIG_IDs") > 0 ]]; then - CONFIG_ID="$(jq --raw-output '.[0]' <<<"$MATCHING_CONFIG_IDs")" - else - CONFIG_ID="$(${occ} ldap:create-empty-config --only-print-prefix)" - fi - - echo "Using configId $CONFIG_ID" - - # The following CLI commands follow - # https://github.com/lldap/lldap/blob/main/example_configs/nextcloud.md#nextcloud-config--the-cli-way - - # StartTLS is not supported in Kanidm due to security risks, whereas - # user_ldap doesn't support SASL. Importing certificate doesn't - # help: - # ${occ} security:certificates:import "${config.security.acme.certs.${domain}.directory}/cert.pem" - ${occ} ldap:set-config "$CONFIG_ID" 'turnOffCertCheck' '1' - - ${occ} ldap:set-config "$CONFIG_ID" 'ldapHost' '${ldap_scheme_and_host}' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapPort' '${toString auth-passthru.ldap-port}' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentName' 'dn=token' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapAgentPassword' "$(<${serviceAccountTokenFP})" - ${occ} ldap:set-config "$CONFIG_ID" 'ldapBase' '${auth-passthru.ldap-base-dn}' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapBaseGroups' '${auth-passthru.ldap-base-dn}' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapBaseUsers' '${auth-passthru.ldap-base-dn}' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapEmailAttribute' 'mail' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilter' \ - '(&(class=group)(${wildcardGroup})' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterGroups' \ - '(&(class=group)(${wildcardGroup}))' - # ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterObjectclass' \ - # 'groupOfUniqueNames' - # ${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupMemberAssocAttr' \ - # 'uniqueMember' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapLoginFilter' \ - '(&(class=person)(memberof=${usersGroup})(uid=%uid))' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapLoginFilterAttributes' \ - 'uid' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserDisplayName' \ - 'displayname' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilter' \ - '(&(class=person)(memberof=${usersGroup})(name=%s))' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterMode' \ - '1' - ${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterObjectclass' \ - 'person' - - ${occ} ldap:test-config -- "$CONFIG_ID" - - # delete all configs except "$CONFIG_ID" - for configid in $(jq --raw-output "keys[] | select(. != \"$CONFIG_ID\")" <<<"$ALL_CONFIG"); do - echo "Deactivating $configid" - ${occ} ldap:set-config "$configid" 'ldapConfigurationActive' '0' - echo "Deactivated $configid" - echo "Deleting $configid" - ${occ} ldap:delete-config "$configid" - echo "Deleted $configid" - done - - ${occ} ldap:set-config "$CONFIG_ID" 'ldapConfigurationActive' '1' - - ############################################################################ - # OIDC app - ############################################################################ - ${occ} app:install user_oidc || : - ${occ} app:enable user_oidc - - ${occ} user_oidc:provider ${auth-passthru.oauth2-provider-name} \ - --clientid="${oauthClientID}" \ - --clientsecret="$(<${oauthClientSecretFP})" \ - --discoveryuri="${auth-passthru.oauth2-discovery-url "nextcloud"}" \ - --unique-uid=0 \ - --scope="email openid profile" \ - --mapping-uid=preferred_username \ - --no-interaction \ - --mapping-groups=groups \ - --group-provisioning=1 \ - -vvv - - '' - (lib.optionalString deleteNextcloudAdmin '' - if [[ ! -f /var/lib/nextcloud/.admin-user-deleted ]]; then - ${occ} user:delete admin - touch /var/lib/nextcloud/.admin-user-deleted - fi - '') - ]; - }; - selfprivacy.auth.clients."${oauthClientID}" = { - inherit adminsGroup usersGroup; - imageFile = ./icon.svg; - displayName = "Nextcloud"; - subdomain = cfg.subdomain; - isTokenNeeded = true; - originUrl = "https://${cfg.subdomain}.${domain}/apps/user_oidc/code"; - originLanding = - "https://${cfg.subdomain}.${domain}/apps/user_oidc/login/1"; - useShortPreferredUsername = true; - clientSystemdUnits = - [ "nextcloud-setup.service" "phpfpm-nextcloud.service" ]; - enablePkce = true; - linuxUserOfClient = linuxUserOfService; - linuxGroupOfClient = linuxGroupOfService; - scopeMaps.${usersGroup} = [ "email" "openid" "profile" ]; - claimMaps.groups = { - joinType = "array"; - valuesByGroup.${adminsGroup} = [ "admin" ]; - }; - }; - }) - (lib.mkIf (! is-auth-enabled) { - systemd.services.nextcloud-setup = { - script = '' - ${occ} app:disable logreader - ${occ} app:disable user_oidc - ''; - }; - }) - ]); + }) + ] + ); } diff --git a/sp-modules/ocserv/flake.nix b/sp-modules/ocserv/flake.nix index 4b7fc03..62dc0d8 100644 --- a/sp-modules/ocserv/flake.nix +++ b/sp-modules/ocserv/flake.nix @@ -1,29 +1,32 @@ { description = "PoC SP module for OpenConnect VPN server (ocserv)"; - outputs = { self }: { - nixosModules.default = import ./module.nix; - configPathsNeeded = - builtins.fromJSON (builtins.readFile ./config-paths-needed.json); - meta = { lib, ... }: { - spModuleSchemaVersion = 1; - id = "ocserv"; - name = "OpenConnect VPN"; - description = "OpenConnect VPN to connect your devices and access the internet."; - svgIcon = builtins.readFile ./icon.svg; - isMovable = false; - isRequired = false; - canBeBackedUp = false; - backupDescription = "Backups are not available for OpenConnect VPN."; - systemdServices = [ - "ocserv.service" - ]; - license = [ - lib.licenses.gpl2Plus - ]; - homepage = "https://gitlab.com/openconnect/ocserv"; - sourcePage = "https://gitlab.com/openconnect/ocserv"; - supportLevel = "deprecated"; + outputs = + { self }: + { + nixosModules.default = import ./module.nix; + configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json); + meta = + { lib, ... }: + { + spModuleSchemaVersion = 1; + id = "ocserv"; + name = "OpenConnect VPN"; + description = "OpenConnect VPN to connect your devices and access the internet."; + svgIcon = builtins.readFile ./icon.svg; + isMovable = false; + isRequired = false; + canBeBackedUp = false; + backupDescription = "Backups are not available for OpenConnect VPN."; + systemdServices = [ + "ocserv.service" + ]; + license = [ + lib.licenses.gpl2Plus + ]; + homepage = "https://gitlab.com/openconnect/ocserv"; + sourcePage = "https://gitlab.com/openconnect/ocserv"; + supportLevel = "deprecated"; + }; }; - }; } diff --git a/sp-modules/ocserv/module.nix b/sp-modules/ocserv/module.nix index b45fdd7..16d4def 100644 --- a/sp-modules/ocserv/module.nix +++ b/sp-modules/ocserv/module.nix @@ -7,15 +7,17 @@ let in { options.selfprivacy.modules.ocserv = { - enable = (lib.mkOption { - default = false; - type = lib.types.bool; - description = "Enable"; - }) // { - meta = { - type = "enable"; + enable = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable"; + }) + // { + meta = { + type = "enable"; + }; }; - }; }; config = lib.mkIf cfg.enable { @@ -66,7 +68,10 @@ in systemd = { services = { ocserv = { - unitConfig.ConditionPathExists = [ cert key ]; + unitConfig.ConditionPathExists = [ + cert + key + ]; serviceConfig.Slice = "ocserv.slice"; }; }; diff --git a/sp-modules/pleroma/cleanup-module.nix b/sp-modules/pleroma/cleanup-module.nix index 52839b8..9848f53 100644 --- a/sp-modules/pleroma/cleanup-module.nix +++ b/sp-modules/pleroma/cleanup-module.nix @@ -5,13 +5,8 @@ in # FIXME do we really want to delete passwords on module deactivation!? { config = lib.mkIf (!sp.modules.pleroma.enable) { - system.activationScripts.pleroma = - lib.trivial.warn - ( - "pleroma service is disabled, ${secrets-exs} will be removed!" - ) - '' - rm -f -v ${secrets-exs} - ''; + system.activationScripts.pleroma = lib.trivial.warn ("pleroma service is disabled, ${secrets-exs} will be removed!") '' + rm -f -v ${secrets-exs} + ''; }; } diff --git a/sp-modules/pleroma/common.nix b/sp-modules/pleroma/common.nix index fb29361..ff68e8f 100644 --- a/sp-modules/pleroma/common.nix +++ b/sp-modules/pleroma/common.nix @@ -1,5 +1,4 @@ -config: -{ +config: { sp = config.selfprivacy; secrets-exs = "/var/lib/pleroma/secrets.exs"; } diff --git a/sp-modules/pleroma/flake.nix b/sp-modules/pleroma/flake.nix index 23ab510..104fe0f 100644 --- a/sp-modules/pleroma/flake.nix +++ b/sp-modules/pleroma/flake.nix @@ -1,35 +1,38 @@ { description = "PoC SP module for Pleroma lightweight fediverse server"; - outputs = { self }: { - nixosModules.default = import ./module.nix; - configPathsNeeded = - builtins.fromJSON (builtins.readFile ./config-paths-needed.json); - meta = { lib, ... }: { - spModuleSchemaVersion = 1; - id = "pleroma"; - name = "Pleroma"; - description = "Pleroma is a microblogging service that offers a web interface and a desktop client."; - svgIcon = builtins.readFile ./icon.svg; - isMovable = true; - isRequired = false; - canBeBackedUp = true; - backupDescription = "Your Pleroma accounts, posts and media."; - systemdServices = [ - "pleroma.service" - ]; - folders = [ - "/var/lib/pleroma" - ]; - postgreDatabases = [ - "pleroma" - ]; - license = [ - lib.licenses.agpl3Only - ]; - homepage = "https://pleroma.social/"; - sourcePage = "https://git.pleroma.social/pleroma/pleroma"; - supportLevel = "deprecated"; + outputs = + { self }: + { + nixosModules.default = import ./module.nix; + configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json); + meta = + { lib, ... }: + { + spModuleSchemaVersion = 1; + id = "pleroma"; + name = "Pleroma"; + description = "Pleroma is a microblogging service that offers a web interface and a desktop client."; + svgIcon = builtins.readFile ./icon.svg; + isMovable = true; + isRequired = false; + canBeBackedUp = true; + backupDescription = "Your Pleroma accounts, posts and media."; + systemdServices = [ + "pleroma.service" + ]; + folders = [ + "/var/lib/pleroma" + ]; + postgreDatabases = [ + "pleroma" + ]; + license = [ + lib.licenses.agpl3Only + ]; + homepage = "https://pleroma.social/"; + sourcePage = "https://git.pleroma.social/pleroma/pleroma"; + supportLevel = "deprecated"; + }; }; - }; } diff --git a/sp-modules/pleroma/module.nix b/sp-modules/pleroma/module.nix index 3621dff..a8b8c29 100644 --- a/sp-modules/pleroma/module.nix +++ b/sp-modules/pleroma/module.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let secrets-filepath = "/etc/selfprivacy/secrets.json"; cfg = config.selfprivacy.modules.pleroma; @@ -6,35 +11,41 @@ let in { options.selfprivacy.modules.pleroma = { - enable = (lib.mkOption { - default = false; - type = lib.types.bool; - description = "Enable"; - }) // { - meta = { - type = "enable"; + enable = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable"; + }) + // { + meta = { + type = "enable"; + }; }; - }; - location = (lib.mkOption { - type = lib.types.str; - description = "Location"; - }) // { - meta = { - type = "location"; + location = + (lib.mkOption { + type = lib.types.str; + description = "Location"; + }) + // { + meta = { + type = "location"; + }; }; - }; - subdomain = (lib.mkOption { - default = "social"; - 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; + subdomain = + (lib.mkOption { + default = "social"; + 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; + }; }; - }; }; config = lib.mkIf cfg.enable { fileSystems = lib.mkIf sp.useBinds { @@ -55,10 +66,9 @@ in user = "pleroma"; group = "pleroma"; configs = [ - (builtins.replaceStrings - [ "$DOMAIN" "$LUSER" ] - [ sp.domain sp.username ] - (builtins.readFile ./config.exs.in)) + (builtins.replaceStrings [ "$DOMAIN" "$LUSER" ] [ sp.domain sp.username ] ( + builtins.readFile ./config.exs.in + )) ]; }; postgresql = { @@ -94,7 +104,10 @@ in before = [ "pleroma.service" ]; requiredBy = [ "pleroma.service" ]; serviceConfig.Type = "oneshot"; - path = with pkgs; [ coreutils jq ]; + path = with pkgs; [ + coreutils + jq + ]; script = '' set -o nounset diff --git a/sp-modules/roundcube/flake.nix b/sp-modules/roundcube/flake.nix index d20f919..b72fd49 100644 --- a/sp-modules/roundcube/flake.nix +++ b/sp-modules/roundcube/flake.nix @@ -1,36 +1,39 @@ { description = "Roundcube is a web-based email client."; - outputs = { self }: { - nixosModules.default = import ./module.nix; - configPathsNeeded = - builtins.fromJSON (builtins.readFile ./config-paths-needed.json); - meta = { lib, ... }: { - spModuleSchemaVersion = 1; - id = "roundcube"; - name = "Roundcube"; - description = "Roundcube is an open source webmail software."; - svgIcon = builtins.readFile ./icon.svg; - isMovable = false; - isRequired = false; - canBeBackedUp = true; - backupDescription = "Users' settings."; - postgreDatabases = [ - "roundcube" - ]; - systemdServices = [ - "phpfpm-roundcube.service" - ]; - license = [ - lib.licenses.gpl3 - ]; - homepage = "https://roundcube.net/"; - sourcePage = "https://github.com/roundcube/roundcubemail"; - supportLevel = "normal"; - sso = { - userGroup = "sp.roundcube.users"; - adminGroup = "sp.roundcube.admins"; - }; + outputs = + { self }: + { + nixosModules.default = import ./module.nix; + configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json); + meta = + { lib, ... }: + { + spModuleSchemaVersion = 1; + id = "roundcube"; + name = "Roundcube"; + description = "Roundcube is an open source webmail software."; + svgIcon = builtins.readFile ./icon.svg; + isMovable = false; + isRequired = false; + canBeBackedUp = true; + backupDescription = "Users' settings."; + postgreDatabases = [ + "roundcube" + ]; + systemdServices = [ + "phpfpm-roundcube.service" + ]; + license = [ + lib.licenses.gpl3 + ]; + homepage = "https://roundcube.net/"; + sourcePage = "https://github.com/roundcube/roundcubemail"; + supportLevel = "normal"; + sso = { + userGroup = "sp.roundcube.users"; + adminGroup = "sp.roundcube.admins"; + }; + }; }; - }; } diff --git a/sp-modules/roundcube/module.nix b/sp-modules/roundcube/module.nix index 3e9babc..f38ff48 100644 --- a/sp-modules/roundcube/module.nix +++ b/sp-modules/roundcube/module.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: let domain = config.selfprivacy.domain; cfg = config.selfprivacy.modules.roundcube; @@ -16,129 +21,136 @@ let usersGroup = "sp.${sp-module-name}.users"; oauth-donor = config.selfprivacy.passthru.mailserver; - oauthClientSecretFP = - auth-passthru.mkOAuth2ClientSecretFP linuxGroupOfService; + oauthClientSecretFP = auth-passthru.mkOAuth2ClientSecretFP linuxGroupOfService; # copy client secret from mailserver - kanidmExecStartPreScriptRoot = pkgs.writeShellScript - "${sp-module-name}-kanidm-ExecStartPre-root-script.sh" - '' - install -v -m640 -o kanidm -g ${linuxGroupOfService} ${oauth-donor.oauth-client-secret-fp} ${oauthClientSecretFP} - ''; + kanidmExecStartPreScriptRoot = pkgs.writeShellScript "${sp-module-name}-kanidm-ExecStartPre-root-script.sh" '' + install -v -m640 -o kanidm -g ${linuxGroupOfService} ${oauth-donor.oauth-client-secret-fp} ${oauthClientSecretFP} + ''; in { options.selfprivacy.modules.roundcube = { - enable = (lib.mkOption { - default = false; - type = lib.types.bool; - description = "Enable"; - }) // { - meta = { - type = "enable"; - }; - }; - subdomain = (lib.mkOption { - default = "roundcube"; - 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; - }; - }; - enableSso = (lib.mkOption { - default = false; - type = lib.types.bool; - description = "Enable Single Sign-On"; - }) // { - meta = { - type = "bool"; - weight = 1; - }; - }; - }; - - config = lib.mkIf cfg.enable (lib.mkMerge [ - { - assertions = [ - { - assertion = cfg.enableSso -> config.selfprivacy.sso.enable; - message = - "SSO cannot be enabled for Roundcube when SSO is disabled globally."; - } - ]; - services.roundcube = { - enable = true; - # this is the url of the vhost, not necessarily the same as the fqdn of - # the mailserver - hostName = "${cfg.subdomain}.${config.selfprivacy.domain}"; - extraConfig = '' - # starttls needed for authentication, so the fqdn required to match - # the certificate - $config['smtp_host'] = "tls://${config.mailserver.fqdn}"; - $config['smtp_user'] = "%u"; - $config['smtp_pass'] = "%p"; - ''; - }; - - services.nginx.virtualHosts."${cfg.subdomain}.${domain}" = { - forceSSL = true; - useACMEHost = domain; - enableACME = false; - }; - - systemd.slices.roundcube.description = "Roundcube service slice"; - # Roundcube depends on Dovecot and its OAuth2 client secret. - systemd.services.phpfpm-roundcube.after = [ "dovecot2.service" ]; - } - # the following part is active only when "auth" module is enabled - (lib.mkIf is-auth-enabled { - services.roundcube.extraConfig = lib.mkAfter '' - $config['oauth_provider'] = 'generic'; - $config['oauth_provider_name'] = '${auth-passthru.oauth2-provider-name}'; - $config['oauth_client_id'] = '${oauth-donor.oauth-client-id}'; - $config['oauth_client_secret'] = file_get_contents('${oauthClientSecretFP}'); - $config['oauth_auth_uri'] = 'https://${auth-fqdn}/ui/oauth2'; - $config['oauth_token_uri'] = 'https://${auth-fqdn}/oauth2/token'; - $config['oauth_identity_uri'] = 'https://${auth-fqdn}/oauth2/openid/${oauth-donor.oauth-client-id}/userinfo'; - $config['oauth_scope'] = 'email profile openid'; - $config['oauth_auth_parameters'] = []; - $config['oauth_identity_fields'] = ['email']; - $config['oauth_login_redirect'] = true; - $config['auto_create_user'] = true; - ''; - systemd.services.roundcube = { - after = [ "dovecot2.service" ]; - requires = [ "dovecot2.service" ]; - }; - systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkAfter [ - ("-+" + kanidmExecStartPreScriptRoot) - ]; - - selfprivacy.auth.clients."${oauth-donor.oauth-client-id}" = { - inherit adminsGroup usersGroup; - imageFile = ./icon.svg; - displayName = "Roundcube"; - subdomain = cfg.subdomain; - isTokenNeeded = false; - isMailserver = true; - originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth"; - originLanding = "https://${cfg.subdomain}.${domain}/"; - useShortPreferredUsername = false; - clientSystemdUnits = [ "dovecot2.service" "phpfpm-roundcube.service" ]; - enablePkce = false; - linuxUserOfClient = linuxUserOfService; - linuxGroupOfClient = linuxGroupOfService; - scopeMaps = { - "${usersGroup}" = [ - "email" - "openid" - "profile" - ]; + enable = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable"; + }) + // { + meta = { + type = "enable"; }; }; - }) - ]); + subdomain = + (lib.mkOption { + default = "roundcube"; + 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; + }; + }; + enableSso = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable Single Sign-On"; + }) + // { + meta = { + type = "bool"; + weight = 1; + }; + }; + }; + + config = lib.mkIf cfg.enable ( + lib.mkMerge [ + { + assertions = [ + { + assertion = cfg.enableSso -> config.selfprivacy.sso.enable; + message = "SSO cannot be enabled for Roundcube when SSO is disabled globally."; + } + ]; + services.roundcube = { + enable = true; + # this is the url of the vhost, not necessarily the same as the fqdn of + # the mailserver + hostName = "${cfg.subdomain}.${config.selfprivacy.domain}"; + extraConfig = '' + # starttls needed for authentication, so the fqdn required to match + # the certificate + $config['smtp_host'] = "tls://${config.mailserver.fqdn}"; + $config['smtp_user'] = "%u"; + $config['smtp_pass'] = "%p"; + ''; + }; + + services.nginx.virtualHosts."${cfg.subdomain}.${domain}" = { + forceSSL = true; + useACMEHost = domain; + enableACME = false; + }; + + systemd.slices.roundcube.description = "Roundcube service slice"; + # Roundcube depends on Dovecot and its OAuth2 client secret. + systemd.services.phpfpm-roundcube.after = [ "dovecot2.service" ]; + } + # the following part is active only when "auth" module is enabled + (lib.mkIf is-auth-enabled { + services.roundcube.extraConfig = lib.mkAfter '' + $config['oauth_provider'] = 'generic'; + $config['oauth_provider_name'] = '${auth-passthru.oauth2-provider-name}'; + $config['oauth_client_id'] = '${oauth-donor.oauth-client-id}'; + $config['oauth_client_secret'] = file_get_contents('${oauthClientSecretFP}'); + $config['oauth_auth_uri'] = 'https://${auth-fqdn}/ui/oauth2'; + $config['oauth_token_uri'] = 'https://${auth-fqdn}/oauth2/token'; + $config['oauth_identity_uri'] = 'https://${auth-fqdn}/oauth2/openid/${oauth-donor.oauth-client-id}/userinfo'; + $config['oauth_scope'] = 'email profile openid'; + $config['oauth_auth_parameters'] = []; + $config['oauth_identity_fields'] = ['email']; + $config['oauth_login_redirect'] = true; + $config['auto_create_user'] = true; + ''; + systemd.services.roundcube = { + after = [ "dovecot2.service" ]; + requires = [ "dovecot2.service" ]; + }; + systemd.services.kanidm.serviceConfig.ExecStartPre = lib.mkAfter [ + ("-+" + kanidmExecStartPreScriptRoot) + ]; + + selfprivacy.auth.clients."${oauth-donor.oauth-client-id}" = { + inherit adminsGroup usersGroup; + imageFile = ./icon.svg; + displayName = "Roundcube"; + subdomain = cfg.subdomain; + isTokenNeeded = false; + isMailserver = true; + originUrl = "https://${cfg.subdomain}.${domain}/index.php/login/oauth"; + originLanding = "https://${cfg.subdomain}.${domain}/"; + useShortPreferredUsername = false; + clientSystemdUnits = [ + "dovecot2.service" + "phpfpm-roundcube.service" + ]; + enablePkce = false; + linuxUserOfClient = linuxUserOfService; + linuxGroupOfClient = linuxGroupOfService; + scopeMaps = { + "${usersGroup}" = [ + "email" + "openid" + "profile" + ]; + }; + }; + }) + ] + ); } diff --git a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix index c0d6c20..5c1a8e0 100644 --- a/sp-modules/simple-nixos-mailserver/auth-dovecot.nix +++ b/sp-modules/simple-nixos-mailserver/auth-dovecot.nix @@ -1,8 +1,14 @@ -{ mailserver-service-account-name -, mailserver-service-account-token-name -, mailserver-service-account-token-fp +{ + mailserver-service-account-name, + mailserver-service-account-token-name, + mailserver-service-account-token-fp, }: -{ config, lib, pkgs, ... }@nixos-args: +{ + config, + lib, + pkgs, + ... +}@nixos-args: let inherit (import ./common.nix nixos-args) appendSetting @@ -17,67 +23,70 @@ let keysPath = auth-passthru.keys-path; # create service account token, needed for LDAP - kanidmExecStartPostScript = pkgs.writeShellScript - "mailserver-kanidm-ExecStartPost-script.sh" - '' - export HOME=$RUNTIME_DIRECTORY/client_home - readonly KANIDM="${pkgs.kanidm}/bin/kanidm" + kanidmExecStartPostScript = pkgs.writeShellScript "mailserver-kanidm-ExecStartPost-script.sh" '' + export HOME=$RUNTIME_DIRECTORY/client_home + readonly KANIDM="${pkgs.kanidm}/bin/kanidm" - # get Kanidm service account for mailserver - KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${mailserver-service-account-name}$")" - echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT" - if [ -n "$KANIDM_SERVICE_ACCOUNT" ] - then - echo "kanidm service account \"${mailserver-service-account-name}\" is found" - else - echo "kanidm service account \"${mailserver-service-account-name}\" is not found" - echo "creating new kanidm service account \"${mailserver-service-account-name}\"" - if $KANIDM service-account create --name idm_admin ${mailserver-service-account-name} ${mailserver-service-account-name} idm_admin - then - "kanidm service account \"${mailserver-service-account-name}\" created" - else - echo "error: cannot create kanidm service account \"${mailserver-service-account-name}\"" - exit 1 - fi - fi + # get Kanidm service account for mailserver + KANIDM_SERVICE_ACCOUNT="$($KANIDM service-account list --name idm_admin | grep -E "^name: ${mailserver-service-account-name}$")" + echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT" + if [ -n "$KANIDM_SERVICE_ACCOUNT" ] + then + echo "kanidm service account \"${mailserver-service-account-name}\" is found" + else + echo "kanidm service account \"${mailserver-service-account-name}\" is not found" + echo "creating new kanidm service account \"${mailserver-service-account-name}\"" + if $KANIDM service-account create --name idm_admin ${mailserver-service-account-name} ${mailserver-service-account-name} idm_admin + then + "kanidm service account \"${mailserver-service-account-name}\" created" + else + echo "error: cannot create kanidm service account \"${mailserver-service-account-name}\"" + exit 1 + fi + fi - # add Kanidm service account to `idm_mail_servers` group - $KANIDM group add-members idm_mail_servers ${mailserver-service-account-name} + # add Kanidm service account to `idm_mail_servers` group + $KANIDM group add-members idm_mail_servers ${mailserver-service-account-name} - # create a new read-only token for mailserver - if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin ${mailserver-service-account-name} ${mailserver-service-account-token-name} --output json)" - then - echo "error: kanidm CLI returns an error when trying to generate service-account api-token" - exit 1 - fi - if ! KANIDM_SERVICE_ACCOUNT_TOKEN="$(echo "$KANIDM_SERVICE_ACCOUNT_TOKEN_JSON" | ${lib.getExe pkgs.jq} -r .result)" - then - echo "error: cannot get service-account API token from JSON" - exit 1 - fi + # create a new read-only token for mailserver + if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin ${mailserver-service-account-name} ${mailserver-service-account-token-name} --output json)" + then + echo "error: kanidm CLI returns an error when trying to generate service-account api-token" + exit 1 + fi + if ! KANIDM_SERVICE_ACCOUNT_TOKEN="$(echo "$KANIDM_SERVICE_ACCOUNT_TOKEN_JSON" | ${lib.getExe pkgs.jq} -r .result)" + then + echo "error: cannot get service-account API token from JSON" + exit 1 + fi - if ! install --mode=640 \ - <(printf "%s" "$KANIDM_SERVICE_ACCOUNT_TOKEN") \ - ${mailserver-service-account-token-fp} - then - echo "error: cannot write token to \"${mailserver-service-account-token-fp}\"" - exit 1 - fi - ''; + if ! install --mode=640 \ + <(printf "%s" "$KANIDM_SERVICE_ACCOUNT_TOKEN") \ + ${mailserver-service-account-token-fp} + then + echo "error: cannot write token to \"${mailserver-service-account-token-fp}\"" + exit 1 + fi + ''; ldapConfFile = "/run/${runtime-folder}/dovecot-ldap.conf.ext"; - mkLdapSearchScope = scope: ( - if scope == "sub" then "subtree" - else if scope == "one" then "onelevel" - else scope - ); + mkLdapSearchScope = + scope: + ( + if scope == "sub" then + "subtree" + else if scope == "one" then + "onelevel" + else + scope + ); dovecot-ldap-config = pkgs.writeTextFile { name = "dovecot-ldap.conf.ext.template"; text = '' ldap_version = 3 uris = ${lib.concatStringsSep " " config.mailserver.ldap.uris} ${lib.optionalString config.mailserver.ldap.startTls '' - tls = yes + tls = yes ''} tls_require_cert = hard tls_ca_cert_file = ${config.mailserver.ldap.tlsCAFile} @@ -87,7 +96,7 @@ let base = ${config.mailserver.ldap.searchBase} scope = ${mkLdapSearchScope config.mailserver.ldap.searchScope} ${lib.optionalString (config.mailserver.ldap.dovecot.userAttrs != null) '' - user_attrs = ${config.mailserver.ldap.dovecot.userAttrs} + user_attrs = ${config.mailserver.ldap.dovecot.userAttrs} ''} user_filter = ${config.mailserver.ldap.dovecot.userFilter} ''; @@ -101,10 +110,8 @@ let destination = ldapConfFile; }; oauth-client-id = "mailserver"; - oauth-client-secret-fp = - "${keysPath}/${group}/kanidm-oauth-client-secret"; - oauth-secret-ExecStartPreScript = pkgs.writeShellScript - "${oauth-client-id}-kanidm-ExecStartPre-script.sh" '' + oauth-client-secret-fp = "${keysPath}/${group}/kanidm-oauth-client-secret"; + oauth-secret-ExecStartPreScript = pkgs.writeShellScript "${oauth-client-id}-kanidm-ExecStartPre-script.sh" '' set -o xtrace [ -f "${oauth-client-secret-fp}" ] || \ "${lib.getExe pkgs.openssl}" rand -base64 32 | tr "\n:@/+=" "012345" > "${oauth-client-secret-fp}" @@ -122,8 +129,8 @@ let openid_configuration_url = ${auth-passthru.oauth2-discovery-url oauth-client-id} debug = "no" ''; - prefix = ''introspection_url = "'' + - (auth-passthru.oauth2-introspection-url-prefix oauth-client-id); + prefix = + ''introspection_url = "'' + (auth-passthru.oauth2-introspection-url-prefix oauth-client-id); suffix = auth-passthru.oauth2-introspection-url-postfix + ''"''; passwordFile = oauth-client-secret-fp; destination = dovecot-oauth2-conf-fp; diff --git a/sp-modules/simple-nixos-mailserver/auth-postfix.nix b/sp-modules/simple-nixos-mailserver/auth-postfix.nix index 1d1029e..818c017 100644 --- a/sp-modules/simple-nixos-mailserver/auth-postfix.nix +++ b/sp-modules/simple-nixos-mailserver/auth-postfix.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }@nixos-args: +{ + config, + lib, + pkgs, + ... +}@nixos-args: let inherit (import ./common.nix nixos-args) appendSetting @@ -9,8 +14,7 @@ let cfg = config.mailserver; ldapSenderLoginMapFile = "/run/postfix/ldap-sender-login-map.cf"; - submissionOptions.smtpd_sender_login_maps = - lib.mkForce "hash:/etc/postfix/vaccounts,ldap:${ldapSenderLoginMapFile}"; + submissionOptions.smtpd_sender_login_maps = lib.mkForce "hash:/etc/postfix/vaccounts,ldap:${ldapSenderLoginMapFile}"; commonLdapConfig = '' server_host = ${lib.concatStringsSep " " cfg.ldap.uris} start_tls = ${if cfg.ldap.startTls then "yes" else "no"} @@ -61,8 +65,10 @@ in ${appendPwdInVirtualMailboxMap} ${appendPwdInSenderLoginMap} ''; - restartTriggers = - [ appendPwdInVirtualMailboxMap appendPwdInSenderLoginMap ]; + restartTriggers = [ + appendPwdInVirtualMailboxMap + appendPwdInSenderLoginMap + ]; wants = [ auth-passthru.oauth2-systemd-service ]; after = [ auth-passthru.oauth2-systemd-service ]; }; diff --git a/sp-modules/simple-nixos-mailserver/common.nix b/sp-modules/simple-nixos-mailserver/common.nix index 6e60dba..36b2eaf 100644 --- a/sp-modules/simple-nixos-mailserver/common.nix +++ b/sp-modules/simple-nixos-mailserver/common.nix @@ -4,11 +4,17 @@ rec { domain = config.selfprivacy.domain; group = "dovecot2"; is-auth-enabled = - config.selfprivacy.modules.simple-nixos-mailserver.enableSso - && config.selfprivacy.sso.enable; + config.selfprivacy.modules.simple-nixos-mailserver.enableSso && config.selfprivacy.sso.enable; appendSetting = - { name, file, prefix, suffix ? "", passwordFile, destination }: + { + name, + file, + prefix, + suffix ? "", + passwordFile, + destination, + }: pkgs.writeScript "append-ldap-bind-pwd-in-${name}" '' #!${pkgs.stdenv.shell} set -euo pipefail diff --git a/sp-modules/simple-nixos-mailserver/config.nix b/sp-modules/simple-nixos-mailserver/config.nix index 37d5e8a..f49096f 100644 --- a/sp-modules/simple-nixos-mailserver/config.nix +++ b/sp-modules/simple-nixos-mailserver/config.nix @@ -1,4 +1,9 @@ -{ config, lib, pkgs, ... }@nixos-args: +{ + config, + lib, + pkgs, + ... +}@nixos-args: let sp = config.selfprivacy; @@ -11,26 +16,22 @@ let mailserver-service-account = { mailserver-service-account-name = "sp.mailserver.service-account"; mailserver-service-account-token-name = "mailserver-service-account-token"; - mailserver-service-account-token-fp = - "/run/keys/${group}/kanidm-service-account-token"; # FIXME sync with auth module + mailserver-service-account-token-fp = "/run/keys/${group}/kanidm-service-account-token"; # FIXME sync with auth module }; in -lib.mkIf sp.modules.simple-nixos-mailserver.enable (lib.mkMerge [ - { - assertions = [ - { - assertion = - config.selfprivacy.modules.simple-nixos-mailserver.enableSso - -> config.selfprivacy.sso.enable; - message = - "SSO cannot be enabled for Mailserver when SSO is disabled globally."; - } - ]; - fileSystems = lib.mkIf sp.useBinds - { +lib.mkIf sp.modules.simple-nixos-mailserver.enable ( + lib.mkMerge [ + { + assertions = [ + { + assertion = + config.selfprivacy.modules.simple-nixos-mailserver.enableSso -> config.selfprivacy.sso.enable; + message = "SSO cannot be enabled for Mailserver when SSO is disabled globally."; + } + ]; + fileSystems = lib.mkIf sp.useBinds { "/var/vmail" = { - device = - "/volumes/${sp.modules.simple-nixos-mailserver.location}/vmail"; + device = "/volumes/${sp.modules.simple-nixos-mailserver.location}/vmail"; options = [ "bind" "x-systemd.required-by=postfix.service" @@ -38,8 +39,7 @@ lib.mkIf sp.modules.simple-nixos-mailserver.enable (lib.mkMerge [ ]; }; "/var/sieve" = { - device = - "/volumes/${sp.modules.simple-nixos-mailserver.location}/sieve"; + device = "/volumes/${sp.modules.simple-nixos-mailserver.location}/sieve"; options = [ "bind" "x-systemd.required-by=dovecot2.service" @@ -48,114 +48,120 @@ lib.mkIf sp.modules.simple-nixos-mailserver.enable (lib.mkMerge [ }; }; - users.users = { - virtualMail = { - isNormalUser = false; - }; - }; - - users.groups.acmereceivers.members = [ "dovecot2" "postfix" "virtualMail" ]; - - mailserver = { - enable = true; - fqdn = sp.domain; - domains = [ sp.domain ]; - localDnsResolver = false; - - # A list of all login accounts. To create the password hashes, use - # mkpasswd -m sha-512 "super secret password" - loginAccounts = ({ - "${sp.username}@${sp.domain}" = { - hashedPassword = sp.hashedMasterPassword; - sieveScript = '' - require ["fileinto", "mailbox"]; - if header :contains "Chat-Version" "1.0" - { - fileinto :create "DeltaChat"; - stop; - } - ''; + users.users = { + virtualMail = { + isNormalUser = false; }; - } // builtins.listToAttrs (builtins.map - (user: { - name = "${user.username}@${sp.domain}"; - value = { - hashedPassword = user.hashedPassword; - sieveScript = '' - require ["fileinto", "mailbox"]; - if header :contains "Chat-Version" "1.0" - { - fileinto :create "DeltaChat"; - stop; - } - ''; - }; - }) - sp.users)); - - extraVirtualAliases = { - "admin@${sp.domain}" = "${sp.username}@${sp.domain}"; }; - certificateScheme = "manual"; - certificateFile = "/var/lib/acme/root-${sp.domain}/fullchain.pem"; - keyFile = "/var/lib/acme/root-${sp.domain}/key.pem"; + users.groups.acmereceivers.members = [ + "dovecot2" + "postfix" + "virtualMail" + ]; - # Enable IMAP and POP3 - enableImap = true; - enableImapSsl = true; - enablePop3 = false; - enablePop3Ssl = false; - dkimSelector = "selector"; + mailserver = { + enable = true; + fqdn = sp.domain; + domains = [ sp.domain ]; + localDnsResolver = false; - # Enable the ManageSieve protocol - enableManageSieve = true; + # A list of all login accounts. To create the password hashes, use + # mkpasswd -m sha-512 "super secret password" + loginAccounts = ( + { + "${sp.username}@${sp.domain}" = { + hashedPassword = sp.hashedMasterPassword; + sieveScript = '' + require ["fileinto", "mailbox"]; + if header :contains "Chat-Version" "1.0" + { + fileinto :create "DeltaChat"; + stop; + } + ''; + }; + } + // builtins.listToAttrs ( + builtins.map (user: { + name = "${user.username}@${sp.domain}"; + value = { + hashedPassword = user.hashedPassword; + sieveScript = '' + require ["fileinto", "mailbox"]; + if header :contains "Chat-Version" "1.0" + { + fileinto :create "DeltaChat"; + stop; + } + ''; + }; + }) sp.users + ) + ); - virusScanning = false; + extraVirtualAliases = { + "admin@${sp.domain}" = "${sp.username}@${sp.domain}"; + }; - mailDirectory = "/var/vmail"; - }; + certificateScheme = "manual"; + certificateFile = "/var/lib/acme/root-${sp.domain}/fullchain.pem"; + keyFile = "/var/lib/acme/root-${sp.domain}/key.pem"; - systemd = { - services = { - dovecot2.serviceConfig.Slice = "simple_nixos_mailserver.slice"; - postfix.serviceConfig.Slice = "simple_nixos_mailserver.slice"; - rspamd.serviceConfig.Slice = "simple_nixos_mailserver.slice"; - redis-rspamd.serviceConfig.Slice = "simple_nixos_mailserver.slice"; - opendkim.serviceConfig.Slice = "simple_nixos_mailserver.slice"; + # Enable IMAP and POP3 + enableImap = true; + enableImapSsl = true; + enablePop3 = false; + enablePop3Ssl = false; + dkimSelector = "selector"; + + # Enable the ManageSieve protocol + enableManageSieve = true; + + virusScanning = false; + + mailDirectory = "/var/vmail"; }; - slices."simple_nixos_mailserver" = { - name = "simple_nixos_mailserver.slice"; - description = "Simple NixOS Mailserver service slice"; + + systemd = { + services = { + dovecot2.serviceConfig.Slice = "simple_nixos_mailserver.slice"; + postfix.serviceConfig.Slice = "simple_nixos_mailserver.slice"; + rspamd.serviceConfig.Slice = "simple_nixos_mailserver.slice"; + redis-rspamd.serviceConfig.Slice = "simple_nixos_mailserver.slice"; + opendkim.serviceConfig.Slice = "simple_nixos_mailserver.slice"; + }; + slices."simple_nixos_mailserver" = { + name = "simple_nixos_mailserver.slice"; + description = "Simple NixOS Mailserver service slice"; + }; }; - }; - } - # the following parts are active only when "auth" module is enabled - (lib.mkIf is-auth-enabled { - mailserver = { - extraVirtualAliases = lib.mkForce { }; - loginAccounts = lib.mkForce { }; - # LDAP is needed for Postfix to query Kanidm about email address ownership. - # LDAP is needed for Dovecot also. - ldap = { - # false; otherwise, simple-nixos-mailserver enables auth via LDAP - enable = false; + } + # the following parts are active only when "auth" module is enabled + (lib.mkIf is-auth-enabled { + mailserver = { + extraVirtualAliases = lib.mkForce { }; + loginAccounts = lib.mkForce { }; + # LDAP is needed for Postfix to query Kanidm about email address ownership. + # LDAP is needed for Dovecot also. + ldap = { + # false; otherwise, simple-nixos-mailserver enables auth via LDAP + enable = false; - # bind.dn = "uid=mail,ou=persons," + ldap_base_dn; - bind.dn = "dn=token"; - # TODO change in this file should trigger system restart dovecot - bind.passwordFile = - mailserver-service-account.mailserver-service-account-token-fp; + # bind.dn = "uid=mail,ou=persons," + ldap_base_dn; + bind.dn = "dn=token"; + # TODO change in this file should trigger system restart dovecot + bind.passwordFile = mailserver-service-account.mailserver-service-account-token-fp; - # searchBase = "ou=persons," + ldap_base_dn; - searchBase = auth-passthru.ldap-base-dn; # TODO refine this + # searchBase = "ou=persons," + ldap_base_dn; + searchBase = auth-passthru.ldap-base-dn; # TODO refine this - # NOTE: 127.0.0.1 instead of localhost doesn't work (maybe because of TLS) - uris = [ "ldaps://localhost:${toString auth-passthru.ldap-port}" ]; + # NOTE: 127.0.0.1 instead of localhost doesn't work (maybe because of TLS) + uris = [ "ldaps://localhost:${toString auth-passthru.ldap-port}" ]; + }; }; - }; - }) - (lib.mkIf is-auth-enabled - (import ./auth-dovecot.nix mailserver-service-account nixos-args)) - (lib.mkIf is-auth-enabled (import ./auth-postfix.nix nixos-args)) -]) + }) + (lib.mkIf is-auth-enabled (import ./auth-dovecot.nix mailserver-service-account nixos-args)) + (lib.mkIf is-auth-enabled (import ./auth-postfix.nix nixos-args)) + ] +) diff --git a/sp-modules/simple-nixos-mailserver/flake.nix b/sp-modules/simple-nixos-mailserver/flake.nix index abff9aa..4faed46 100644 --- a/sp-modules/simple-nixos-mailserver/flake.nix +++ b/sp-modules/simple-nixos-mailserver/flake.nix @@ -1,43 +1,45 @@ { description = "PoC SP module for the simple-nixos-mailserver"; - inputs.mailserver.url = - gitlab:simple-nixos-mailserver/nixos-mailserver; + inputs.mailserver.url = "gitlab:simple-nixos-mailserver/nixos-mailserver"; - outputs = { self, mailserver }: { - nixosModules.default = _: { - imports = [ - mailserver.nixosModules.default - ./options.nix - ./config.nix - ]; - }; - configPathsNeeded = - builtins.fromJSON (builtins.readFile ./config-paths-needed.json); - meta = { lib, ... }: { - spModuleSchemaVersion = 1; - id = "simple-nixos-mailserver"; - name = "Mail Server"; - description = "E-Mail for company and family."; - svgIcon = builtins.readFile ./icon.svg; - isMovable = true; - isRequired = true; - canBeBackedUp = true; - backupDescription = "Mail boxes and filters."; - systemdServices = [ - "dovecot2.service" - "postfix.service" - ]; - user = "virtualMail"; - folders = [ - "/var/vmail" - "/var/sieve" - ]; - supportLevel = "normal"; - }; + outputs = + { self, mailserver }: + { + nixosModules.default = _: { + imports = [ + mailserver.nixosModules.default + ./options.nix + ./config.nix + ]; + }; + configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json); + meta = + { lib, ... }: + { + spModuleSchemaVersion = 1; + id = "simple-nixos-mailserver"; + name = "Mail Server"; + description = "E-Mail for company and family."; + svgIcon = builtins.readFile ./icon.svg; + isMovable = true; + isRequired = true; + canBeBackedUp = true; + backupDescription = "Mail boxes and filters."; + systemdServices = [ + "dovecot2.service" + "postfix.service" + ]; + user = "virtualMail"; + folders = [ + "/var/vmail" + "/var/sieve" + ]; + supportLevel = "normal"; + }; - # TODO generate json docs from module? something like: - # nix eval --impure --expr 'let flake = builtins.getFlake (builtins.toPath ./.); pkgs = flake.inputs.mailserver.inputs.nixpkgs.legacyPackages.x86_64-linux; in (pkgs.nixosOptionsDoc { inherit (pkgs.lib.evalModules { modules = [ flake.nixosModules.default ]; }) options; }).optionsJSON' - # (doesn't work because of `assertions`) - }; + # TODO generate json docs from module? something like: + # nix eval --impure --expr 'let flake = builtins.getFlake (builtins.toPath ./.); pkgs = flake.inputs.mailserver.inputs.nixpkgs.legacyPackages.x86_64-linux; in (pkgs.nixosOptionsDoc { inherit (pkgs.lib.evalModules { modules = [ flake.nixosModules.default ]; }) options; }).optionsJSON' + # (doesn't work because of `assertions`) + }; } diff --git a/sp-modules/simple-nixos-mailserver/options.nix b/sp-modules/simple-nixos-mailserver/options.nix index 03ca031..02c80ee 100644 --- a/sp-modules/simple-nixos-mailserver/options.nix +++ b/sp-modules/simple-nixos-mailserver/options.nix @@ -1,31 +1,37 @@ { lib, ... }: { options.selfprivacy.modules.simple-nixos-mailserver = { - enable = (lib.mkOption { - default = false; - type = lib.types.bool; - description = "Enable mail server"; - }) // { - meta = { - type = "enable"; + enable = + (lib.mkOption { + default = false; + type = lib.types.bool; + description = "Enable mail server"; + }) + // { + meta = { + type = "enable"; + }; }; - }; - location = (lib.mkOption { - type = lib.types.str; - description = "Location"; - }) // { - meta = { - type = "location"; + location = + (lib.mkOption { + type = lib.types.str; + description = "Location"; + }) + // { + meta = { + type = "location"; + }; }; - }; - enableSso = (lib.mkOption { - default = true; - type = lib.types.bool; - description = "Enable SSO for mail server"; - }) // { - meta = { - type = "enable"; + enableSso = + (lib.mkOption { + default = true; + type = lib.types.bool; + description = "Enable SSO for mail server"; + }) + // { + meta = { + type = "enable"; + }; }; - }; }; } diff --git a/sp-modules/vikunja/flake.nix b/sp-modules/vikunja/flake.nix index 7558a21..f2649be 100644 --- a/sp-modules/vikunja/flake.nix +++ b/sp-modules/vikunja/flake.nix @@ -5,37 +5,40 @@ nixpkgs-24-11.url = "github:NixOS/nixpkgs/nixos-24.11"; }; - outputs = {nixpkgs-24-11, ...}: { - nixosModules.default = import ./module.nix nixpkgs-24-11.legacyPackages.x86_64-linux; - configPathsNeeded = - builtins.fromJSON (builtins.readFile ./config-paths-needed.json); - meta = {lib, ...}: { - spModuleSchemaVersion = 1; - id = "vikunja"; - name = "Vikunja"; - description = "Vikunja, the fluffy, open-source, self-hostable to-do app."; - svgIcon = builtins.readFile ./icon.svg; - isMovable = true; - isRequired = false; - backupDescription = "Tasks and attachments."; - systemdServices = [ - "vikunja.service" - ]; - folders = [ - "/var/lib/vikunja" - ]; - postgreDatabases = [ - "vikunja" - ]; - license = [ - lib.licenses.agpl3Plus - ]; - homepage = "https://vikunja.io"; - sourcePage = "https://github.com/go-vikunja/vikunja"; - supportLevel = "normal"; - sso = { - userGroup = "sp.vikunja.users"; - }; + outputs = + { nixpkgs-24-11, ... }: + { + nixosModules.default = import ./module.nix nixpkgs-24-11.legacyPackages.x86_64-linux; + configPathsNeeded = builtins.fromJSON (builtins.readFile ./config-paths-needed.json); + meta = + { lib, ... }: + { + spModuleSchemaVersion = 1; + id = "vikunja"; + name = "Vikunja"; + description = "Vikunja, the fluffy, open-source, self-hostable to-do app."; + svgIcon = builtins.readFile ./icon.svg; + isMovable = true; + isRequired = false; + backupDescription = "Tasks and attachments."; + systemdServices = [ + "vikunja.service" + ]; + folders = [ + "/var/lib/vikunja" + ]; + postgreDatabases = [ + "vikunja" + ]; + license = [ + lib.licenses.agpl3Plus + ]; + homepage = "https://vikunja.io"; + sourcePage = "https://github.com/go-vikunja/vikunja"; + supportLevel = "normal"; + sso = { + userGroup = "sp.vikunja.users"; + }; + }; }; - }; } diff --git a/sp-modules/vikunja/module.nix b/sp-modules/vikunja/module.nix index e36c5c9..0539cba 100644 --- a/sp-modules/vikunja/module.nix +++ b/sp-modules/vikunja/module.nix @@ -1,8 +1,10 @@ -latestPkgs: { +latestPkgs: +{ config, lib, ... -}: let +}: +let sp = config.selfprivacy; cfg = sp.modules.vikunja; oauthClientID = "vikunja"; @@ -13,18 +15,16 @@ latestPkgs: { # SelfPrivacy uses SP Module ID to identify the group! usersGroup = "sp.vikunja.users"; - oauthClientSecretFP = - auth-passthru.mkOAuth2ClientSecretFP oauthClientID; + oauthClientSecretFP = auth-passthru.mkOAuth2ClientSecretFP oauthClientID; vikunjaPackage = latestPkgs.vikunja.overrideAttrs (old: { doCheck = false; # Tests are slow. - patches = - (old.patches or []) - ++ [ - ./load-client-secret-from-env.patch - ]; + patches = (old.patches or [ ]) ++ [ + ./load-client-secret-from-env.patch + ]; }); -in { +in +{ options.selfprivacy.modules.vikunja = { enable = (lib.mkOption { @@ -63,157 +63,168 @@ in { }; }; - config = - lib.mkIf cfg.enable - { - assertions = [ - { - assertion = sp.sso.enable; - message = "Vikunja cannot be enabled when SSO is disabled."; - } - ]; + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = sp.sso.enable; + message = "Vikunja cannot be enabled when SSO is disabled."; + } + ]; - fileSystems = lib.mkIf sp.useBinds { - "/var/lib/vikunja" = { - device = "/volumes/${cfg.location}/vikunja"; - options = ["bind"]; - }; - }; - - users = { - users.vikunja = { - isSystemUser = true; - group = "vikunja"; - }; - groups.vikunja = {}; - }; - - services.postgresql = { - ensureDatabases = ["vikunja"]; - ensureUsers = [ - { - name = "vikunja"; - ensureDBOwnership = true; - } - ]; - }; - - services.vikunja = { - enable = true; - package = vikunjaPackage; - frontendScheme = "https"; - frontendHostname = "${cfg.subdomain}.${sp.domain}"; - port = 4835; - - database = { - type = "postgres"; - host = "/run/postgresql"; - }; - - settings = { - service = { - enableregistration = false; - enabletotp = false; - enableuserdeletion = true; - }; - - auth = { - local.enabled = false; - openid = { - enabled = true; - providers = [ - { - name = oauth2-provider-name; - authurl = lib.strings.removeSuffix "/.well-known/openid-configuration" oauthDiscoveryURL; - clientid = oauthClientID; - clientsecret = ""; # There's patch for our Vikunja to make it load client secret from environment variable. - scope = "openid profile email"; - } - ]; - }; - }; - }; - }; - - services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = { - useACMEHost = sp.domain; - forceSSL = true; - extraConfig = '' - add_header Strict-Transport-Security $hsts_header; - #add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always; - add_header 'Referrer-Policy' 'origin-when-cross-origin'; - add_header X-Frame-Options DENY; - add_header X-Content-Type-Options nosniff; - add_header X-XSS-Protection "1; mode=block"; - proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; - ''; - locations = { - "/" = { - proxyPass = "http://127.0.0.1:4835"; - }; - }; - }; - - systemd = { - services.vikunja = { - unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/vikunja"; - serviceConfig = { - Slice = "vikunja.slice"; - LoadCredential = "oauth2-secret:${oauthClientSecretFP}"; - DynamicUser = lib.mkForce false; - User = "vikunja"; - Group = "vikunja"; - - AmbientCapabilities = [""]; - - LockPersonality = true; - MemoryDenyWriteExecute = true; - NoNewPrivileges = true; - PrivateDevices = true; - PrivateTmp = true; - - PrivateUsers = true; - ProcSubset = "pid"; - ProtectClock = true; - ProtectControlGroups = true; - ProtectHome = true; - ProtectHostname = true; - ProtectKernelLogs = true; - ProtectKernelModules = true; - ProtectKernelTunables = true; - - ProtectProc = "invisible"; - - ProtectSystem = "strict"; - - RestrictAddressFamilies = ["AF_UNIX" "AF_INET" "AF_INET6"]; - - RestrictNamespaces = true; - RestrictRealtime = true; - RestrictSUIDSGID = true; - SystemCallArchitectures = "native"; - - RemoveIPC = true; - - SystemCallFilter = ["@system-service" "~@cpu-emulation" "~@debug" "~@keyring" "~@memlock" "~@obsolete" "~@privileged" "~@setuid"]; - }; - environment.SP_VIKUNJA_CLIENT_SECRET_PATH = "%d/oauth2-secret"; - }; - slices.vikunja = { - description = "Vikunja service slice"; - }; - }; - - selfprivacy.auth.clients.${oauthClientID} = { - inherit usersGroup; - subdomain = cfg.subdomain; - isTokenNeeded = false; - originLanding = "https://${cfg.subdomain}.${sp.domain}/"; - originUrl = "https://${cfg.subdomain}.${sp.domain}/auth/openid/${lib.strings.toLower oauth2-provider-name}"; - clientSystemdUnits = ["vikunja.service"]; - enablePkce = false; - linuxUserOfClient = "vikunja"; - linuxGroupOfClient = "vikunja"; + fileSystems = lib.mkIf sp.useBinds { + "/var/lib/vikunja" = { + device = "/volumes/${cfg.location}/vikunja"; + options = [ "bind" ]; }; }; + + users = { + users.vikunja = { + isSystemUser = true; + group = "vikunja"; + }; + groups.vikunja = { }; + }; + + services.postgresql = { + ensureDatabases = [ "vikunja" ]; + ensureUsers = [ + { + name = "vikunja"; + ensureDBOwnership = true; + } + ]; + }; + + services.vikunja = { + enable = true; + package = vikunjaPackage; + frontendScheme = "https"; + frontendHostname = "${cfg.subdomain}.${sp.domain}"; + port = 4835; + + database = { + type = "postgres"; + host = "/run/postgresql"; + }; + + settings = { + service = { + enableregistration = false; + enabletotp = false; + enableuserdeletion = true; + }; + + auth = { + local.enabled = false; + openid = { + enabled = true; + providers = [ + { + name = oauth2-provider-name; + authurl = lib.strings.removeSuffix "/.well-known/openid-configuration" oauthDiscoveryURL; + clientid = oauthClientID; + clientsecret = ""; # There's patch for our Vikunja to make it load client secret from environment variable. + scope = "openid profile email"; + } + ]; + }; + }; + }; + }; + + services.nginx.virtualHosts."${cfg.subdomain}.${sp.domain}" = { + useACMEHost = sp.domain; + forceSSL = true; + extraConfig = '' + add_header Strict-Transport-Security $hsts_header; + #add_header Content-Security-Policy "script-src 'self'; object-src 'none'; base-uri 'none';" always; + add_header 'Referrer-Policy' 'origin-when-cross-origin'; + add_header X-Frame-Options DENY; + add_header X-Content-Type-Options nosniff; + add_header X-XSS-Protection "1; mode=block"; + proxy_cookie_path / "/; secure; HttpOnly; SameSite=strict"; + ''; + locations = { + "/" = { + proxyPass = "http://127.0.0.1:4835"; + }; + }; + }; + + systemd = { + services.vikunja = { + unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/vikunja"; + serviceConfig = { + Slice = "vikunja.slice"; + LoadCredential = "oauth2-secret:${oauthClientSecretFP}"; + DynamicUser = lib.mkForce false; + User = "vikunja"; + Group = "vikunja"; + + AmbientCapabilities = [ "" ]; + + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + PrivateDevices = true; + PrivateTmp = true; + + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + + ProtectProc = "invisible"; + + ProtectSystem = "strict"; + + RestrictAddressFamilies = [ + "AF_UNIX" + "AF_INET" + "AF_INET6" + ]; + + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + + RemoveIPC = true; + + SystemCallFilter = [ + "@system-service" + "~@cpu-emulation" + "~@debug" + "~@keyring" + "~@memlock" + "~@obsolete" + "~@privileged" + "~@setuid" + ]; + }; + environment.SP_VIKUNJA_CLIENT_SECRET_PATH = "%d/oauth2-secret"; + }; + slices.vikunja = { + description = "Vikunja service slice"; + }; + }; + + selfprivacy.auth.clients.${oauthClientID} = { + inherit usersGroup; + subdomain = cfg.subdomain; + isTokenNeeded = false; + originLanding = "https://${cfg.subdomain}.${sp.domain}/"; + originUrl = "https://${cfg.subdomain}.${sp.domain}/auth/openid/${lib.strings.toLower oauth2-provider-name}"; + clientSystemdUnits = [ "vikunja.service" ]; + enablePkce = false; + linuxUserOfClient = "vikunja"; + linuxGroupOfClient = "vikunja"; + }; + }; } diff --git a/users.nix b/users.nix index 91fbeab..ffa6a3e 100644 --- a/users.nix +++ b/users.nix @@ -6,21 +6,23 @@ in users = { mutableUsers = false; allowNoPasswordLogin = true; - users = { - "${cfg.username}" = { - isNormalUser = true; - hashedPassword = cfg.hashedMasterPassword; - openssh.authorizedKeys.keys = cfg.sshKeys; - }; - } // builtins.listToAttrs (builtins.map - (user: { - name = "${user.username}"; - value = { + users = + { + "${cfg.username}" = { isNormalUser = true; - hashedPassword = user.hashedPassword; - openssh.authorizedKeys.keys = (if user ? sshKeys then user.sshKeys else [ ]); + hashedPassword = cfg.hashedMasterPassword; + openssh.authorizedKeys.keys = cfg.sshKeys; }; - }) - cfg.users); + } + // builtins.listToAttrs ( + builtins.map (user: { + name = "${user.username}"; + value = { + isNormalUser = true; + hashedPassword = user.hashedPassword; + openssh.authorizedKeys.keys = (if user ? sshKeys then user.sshKeys else [ ]); + }; + }) cfg.users + ); }; } diff --git a/volumes.nix b/volumes.nix index f991579..e5ec621 100644 --- a/volumes.nix +++ b/volumes.nix @@ -3,13 +3,13 @@ let cfg = config.selfprivacy; in { - fileSystems = builtins.listToAttrs (builtins.map - (volume: { + fileSystems = builtins.listToAttrs ( + builtins.map (volume: { name = "${volume.mountPoint}"; value = { device = "${volume.device}"; fsType = "${volume.fsType}"; }; - }) - cfg.volumes); + }) cfg.volumes + ); }