Files
sp-config/sp-modules/nextcloud/module.nix
2025-05-01 16:09:43 +03:00

383 lines
14 KiB
Nix

{ config, lib, pkgs, ... }:
let
inherit (import ./common.nix config)
admin-pass-filepath
db-pass-filepath
domain
override-config-fp
secrets-filepath
sp
;
hostName = "${cfg.subdomain}.${sp.domain}";
auth-passthru = config.selfprivacy.passthru.auth;
deleteNextcloudAdmin = config.selfprivacy.workarounds.deleteNextcloudAdmin;
cfg = sp.modules.nextcloud;
is-auth-enabled = cfg.enableSso && config.selfprivacy.sso.enable;
ldap_scheme_and_host = "ldaps://${auth-passthru.ldap-host}";
occ = "${config.services.nextcloud.occ}/bin/nextcloud-occ";
linuxUserOfService = config.services.phpfpm.pools.nextcloud.user;
linuxGroupOfService = config.services.phpfpm.pools.nextcloud.group;
oauthClientID = "nextcloud";
adminsGroup = "sp.${oauthClientID}.admins";
usersGroup = "sp.${oauthClientID}.users";
wildcardGroup = "sp.${oauthClientID}.*";
serviceAccountTokenFP =
auth-passthru.mkServiceAccountTokenFP linuxUserOfService;
oauthClientSecretFP =
auth-passthru.mkOAuth2ClientSecretFP linuxUserOfService;
in
{
options.selfprivacy.modules.nextcloud = with lib; {
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";
};
};
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;
};
};
enableSso = (lib.mkOption {
default = false;
type = lib.types.bool;
description = "Enable Single Sign-On";
}) // {
meta = {
type = "bool";
weight = 2;
};
};
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;
};
};
};
# 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;
};
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" = 20;
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;
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;
'';
};
}
# 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: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 user_oidc
'';
};
})
]);
}