mirror of
https://git.selfprivacy.org/SelfPrivacy/selfprivacy-nixos-config.git
synced 2025-03-12 17:03:49 +00:00
feat(auth,nextcloud): OAuth2 and LDAP integration
This commit is contained in:
parent
a45cf792e5
commit
153e1c12d5
4 changed files with 351 additions and 85 deletions
sp-modules/nextcloud
|
@ -1,6 +1,11 @@
|
|||
{ config, lib, ... }:
|
||||
let
|
||||
inherit (import ./common.nix config) sp db-pass-filepath admin-pass-filepath;
|
||||
inherit (import ./common.nix config)
|
||||
admin-pass-filepath
|
||||
db-pass-filepath
|
||||
override-config-fp
|
||||
sp
|
||||
;
|
||||
in
|
||||
# FIXME do we really want to delete passwords on module deactivation!?
|
||||
{
|
||||
|
@ -9,11 +14,13 @@ in
|
|||
lib.trivial.warn
|
||||
(
|
||||
"nextcloud service is disabled, " +
|
||||
"${db-pass-filepath} and ${admin-pass-filepath} will be removed!"
|
||||
"${override-config-fp}, ${db-pass-filepath} and ${admin-pass-filepath} will be removed!"
|
||||
)
|
||||
''
|
||||
rm -f -v ${db-pass-filepath}
|
||||
rm -f -v ${admin-pass-filepath}
|
||||
[ ! -f "${override-config-fp}" && -L "${override-config-fp}" ] && \
|
||||
rm -v "${override-config-fp}"
|
||||
'';
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
config: rec {
|
||||
sp = config.selfprivacy;
|
||||
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";
|
||||
override-config-fp = "/var/lib/nextcloud/config/override.config.php";
|
||||
}
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
[
|
||||
[ "passthru", "selfprivacy", "auth" ],
|
||||
[ "selfprivacy", "domain" ],
|
||||
[ "selfprivacy", "modules", "auth", "enable" ],
|
||||
[ "selfprivacy", "modules", "nextcloud" ],
|
||||
[ "selfprivacy", "useBinds" ],
|
||||
[ "selfprivacy", "modules", "nextcloud" ]
|
||||
[ "services", "nextcloud" ],
|
||||
[ "services", "phpfpm", "pools", "nextcloud", "group" ],
|
||||
[ "systemd", "services", "nextcloud" ],
|
||||
[ "systemd", "services", "nextcloud-setup" ]
|
||||
]
|
||||
|
|
|
@ -1,4 +1,89 @@
|
|||
{ 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.passthru.selfprivacy.auth;
|
||||
|
||||
is-auth-enabled = config.selfprivacy.modules.auth.enable;
|
||||
|
||||
occ = "${config.services.nextcloud.occ}/bin/nextcloud-occ";
|
||||
cfg = sp.modules.nextcloud;
|
||||
nextcloud-secret-file = "/var/lib/nextcloud/secrets.json";
|
||||
nextcloud-setup-group =
|
||||
config.systemd.services.nextcloud-setup.serviceConfig.Group;
|
||||
|
||||
admins-group = "sp.nextcloud.admins";
|
||||
users-group = "sp.nextcloud.users";
|
||||
wildcard-group = "sp.nextcloud.*";
|
||||
|
||||
oauth-client-id = "nextcloud";
|
||||
kanidm-service-account-name = "sp.${oauth-client-id}.service-account";
|
||||
kanidm-service-account-token-name = "${oauth-client-id}-service-account-token";
|
||||
kanidm-service-account-token-fp =
|
||||
"/run/keys/${oauth-client-id}/kanidm-service-account-token"; # FIXME sync with auth module
|
||||
kanidmExecStartPostScriptRoot = pkgs.writeShellScript
|
||||
"${oauth-client-id}-kanidm-ExecStartPost-root-script.sh"
|
||||
''
|
||||
# set-group-ID bit allows for kanidm user to create files,
|
||||
mkdir -p -v --mode=u+rwx,g+rs,g-w,o-rwx /run/keys/${oauth-client-id}
|
||||
chown kanidm:${nextcloud-setup-group} /run/keys/${oauth-client-id}
|
||||
'';
|
||||
kanidmExecStartPostScript = pkgs.writeShellScript
|
||||
"${oauth-client-id}-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: ${kanidm-service-account-name}$")"
|
||||
echo KANIDM_SERVICE_ACCOUNT: "$KANIDM_SERVICE_ACCOUNT"
|
||||
if [ -n "$KANIDM_SERVICE_ACCOUNT" ]
|
||||
then
|
||||
echo "kanidm service account \"${kanidm-service-account-name}\" is found"
|
||||
else
|
||||
echo "kanidm service account \"${kanidm-service-account-name}\" is not found"
|
||||
echo "creating new kanidm service account \"${kanidm-service-account-name}\""
|
||||
if $KANIDM service-account create --name idm_admin ${kanidm-service-account-name} ${kanidm-service-account-name} idm_admin
|
||||
then
|
||||
"kanidm service account \"${kanidm-service-account-name}\" created"
|
||||
else
|
||||
echo "error: cannot create kanidm service account \"${kanidm-service-account-name}\""
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# add Kanidm service account to `idm_mail_servers` group
|
||||
$KANIDM group add-members idm_mail_servers ${kanidm-service-account-name}
|
||||
|
||||
# create a new read-only token for kanidm
|
||||
if ! KANIDM_SERVICE_ACCOUNT_TOKEN_JSON="$($KANIDM service-account api-token generate --name idm_admin ${kanidm-service-account-name} ${kanidm-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") \
|
||||
${kanidm-service-account-token-fp}
|
||||
then
|
||||
echo "error: cannot write token to \"${kanidm-service-account-token-fp}\""
|
||||
exit 1
|
||||
fi
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.selfprivacy.modules.nextcloud = with lib; {
|
||||
enable = (lib.mkOption {
|
||||
|
@ -40,90 +125,256 @@
|
|||
weight = 1;
|
||||
};
|
||||
};
|
||||
debug = lib.mkOption {
|
||||
default = false;
|
||||
type = lib.types.bool;
|
||||
};
|
||||
};
|
||||
|
||||
config =
|
||||
let
|
||||
inherit (import ./common.nix config)
|
||||
sp secrets-filepath db-pass-filepath admin-pass-filepath;
|
||||
cfg = sp.modules.nextcloud;
|
||||
hostName = "${cfg.subdomain}.${sp.domain}";
|
||||
in
|
||||
lib.mkIf sp.modules.nextcloud.enable {
|
||||
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"
|
||||
];
|
||||
};
|
||||
};
|
||||
systemd = {
|
||||
services = {
|
||||
phpfpm-nextcloud.serviceConfig.Slice = lib.mkForce "nextcloud.slice";
|
||||
nextcloud-setup.serviceConfig.Slice = "nextcloud.slice";
|
||||
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.nextcloud29;
|
||||
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";
|
||||
|
||||
settings = {
|
||||
# further forces Nextcloud to use HTTPS
|
||||
overwriteprotocol = "https";
|
||||
};
|
||||
|
||||
config = {
|
||||
dbtype = "sqlite";
|
||||
dbuser = "nextcloud";
|
||||
dbname = "nextcloud";
|
||||
dbpassFile = db-pass-filepath;
|
||||
adminpassFile = admin-pass-filepath;
|
||||
adminuser = "admin";
|
||||
};
|
||||
|
||||
enableImagemagick = cfg.enableImagemagick;
|
||||
};
|
||||
services.nginx.virtualHosts.${hostName} = {
|
||||
useACMEHost = sp.domain;
|
||||
forceSSL = true;
|
||||
config = lib.mkIf sp.modules.nextcloud.enable {
|
||||
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 [ nextcloud-setup-group ];
|
||||
systemd = {
|
||||
services = {
|
||||
phpfpm-nextcloud.serviceConfig.Slice = lib.mkForce "nextcloud.slice";
|
||||
nextcloud-setup = {
|
||||
serviceConfig.Slice = "nextcloud.slice";
|
||||
serviceConfig.Group = config.services.phpfpm.pools.nextcloud.group;
|
||||
# FIXME secret
|
||||
preStart = lib.mkIf is-auth-enabled ''
|
||||
cat <<EOF > "${nextcloud-secret-file}"
|
||||
{
|
||||
"oidc_login_client_secret": "VERY-STRONG-SECRET-FOR-NEXTCLOUD"
|
||||
}
|
||||
EOF
|
||||
'';
|
||||
path = lib.mkIf is-auth-enabled [ pkgs.jq ];
|
||||
script = lib.mkIf is-auth-enabled ''
|
||||
${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 --show-password)"
|
||||
|
||||
# TODO investigate this!
|
||||
MATCHING_CONFIG_IDs="$(echo "$ALL_CONFIG" | jq '[to_entries[] | select(.value.ldapHost=="${auth-passthru.ldap-host}") | .key]')"
|
||||
if [[ $(echo "$MATCHING_CONFIG_IDs" | jq 'length') > 0 ]]; then
|
||||
CONFIG_ID="$(echo "$MATCHING_CONFIG_IDs" | jq --raw-output '.[0]')"
|
||||
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
|
||||
|
||||
# FIXME
|
||||
${occ} ldap:set-config "$CONFIG_ID" 'turnOffCertCheck' '1'
|
||||
|
||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapHost' 'ldaps://${auth-passthru.ldap-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' "$(<${kanidm-service-account-token-fp})"
|
||||
${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)(${wildcard-group}))'
|
||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapGroupFilterGroups' \
|
||||
'(&(class=group)(${wildcard-group}))'
|
||||
# ${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=${users-group})(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=${users-group})(name=%s))'
|
||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterMode' \
|
||||
'1'
|
||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapUserFilterObjectclass' \
|
||||
'person'
|
||||
|
||||
${occ} ldap:test-config -- "$CONFIG_ID"
|
||||
|
||||
# Only one active at the same time
|
||||
|
||||
# TODO investigate this! It takes a minute to deactivate all.
|
||||
for configid in $(echo "$ALL_CONFIG" | jq --raw-output "keys[]"); do
|
||||
echo "Deactivating $configid"
|
||||
${occ} ldap:set-config "$configid" 'ldapConfigurationActive' \
|
||||
'0'
|
||||
echo "Deactivated $configid"
|
||||
done
|
||||
|
||||
${occ} ldap:set-config "$CONFIG_ID" 'ldapConfigurationActive' \
|
||||
'1'
|
||||
|
||||
############################################################################
|
||||
# OIDC app
|
||||
############################################################################
|
||||
${occ} app:install user_oidc || :
|
||||
${occ} app:enable user_oidc
|
||||
|
||||
# FIXME clientsecret
|
||||
${occ} user_oidc:provider ${auth-passthru.oauth2-provider-name} \
|
||||
--clientid="${oauth-client-id}" \
|
||||
--clientsecret="VERY-STRONG-SECRET-FOR-NEXTCLOUD" \
|
||||
--discoveryuri="${auth-passthru.oauth2-discovery-url "nextcloud"}" \
|
||||
--unique-uid=0 \
|
||||
--scope="email openid profile" \
|
||||
--mapping-uid=preferred_username \
|
||||
--no-interaction \
|
||||
--mapping-uid=name \
|
||||
--mapping-groups=groups \
|
||||
--group-provisioning=1 \
|
||||
-vvv
|
||||
'';
|
||||
# TODO consider passing oauth consumer service to auth module instead
|
||||
wants = lib.mkIf is-auth-enabled
|
||||
[ auth-passthru.oauth2-systemd-service ];
|
||||
after = lib.mkIf is-auth-enabled
|
||||
[ auth-passthru.oauth2-systemd-service ];
|
||||
};
|
||||
kanidm.serviceConfig.ExecStartPost = lib.mkIf is-auth-enabled
|
||||
(lib.mkAfter [
|
||||
("+" + kanidmExecStartPostScriptRoot)
|
||||
kanidmExecStartPostScript
|
||||
]);
|
||||
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.nextcloud29;
|
||||
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";
|
||||
|
||||
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";
|
||||
};
|
||||
|
||||
secretFile = lib.mkIf is-auth-enabled nextcloud-secret-file;
|
||||
};
|
||||
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;
|
||||
'';
|
||||
};
|
||||
services.kanidm.provision = lib.mkIf is-auth-enabled {
|
||||
groups = {
|
||||
"${admins-group}".members = [ "sp.admins" ];
|
||||
"${users-group}".members = [ admins-group ];
|
||||
};
|
||||
systems.oauth2.${oauth-client-id} = {
|
||||
displayName = "Nextcloud";
|
||||
originUrl = "https://${cfg.subdomain}.${domain}/apps/user_oidc/code";
|
||||
originLanding = "https://${cfg.subdomain}.${domain}/";
|
||||
basicSecretFile = pkgs.writeText "bs-nextcloud" "VERY-STRONG-SECRET-FOR-NEXTCLOUD"; # FIXME
|
||||
# when true, name is passed to a service instead of name@domain
|
||||
preferShortUsername = true;
|
||||
allowInsecureClientDisablePkce = false;
|
||||
scopeMaps.${users-group} = [ "email" "openid" "profile" ];
|
||||
removeOrphanedClaimMaps = true;
|
||||
claimMaps.groups = {
|
||||
joinType = "array";
|
||||
valuesByGroup.${admins-group} = [ "admin" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue