nextcloud: fix ldap and sso integration and add playwright tests
This commit is contained in:
parent
7717e39671
commit
ebd39bbe73
6 changed files with 221 additions and 30 deletions
|
|
@ -77,6 +77,15 @@
|
|||
"blocks-authelia-options-shb.authelia.enable": [
|
||||
"blocks-authelia.html#blocks-authelia-options-shb.authelia.enable"
|
||||
],
|
||||
"blocks-authelia-options-shb.authelia.extraDefinitions": [
|
||||
"blocks-authelia.html#blocks-authelia-options-shb.authelia.extraDefinitions"
|
||||
],
|
||||
"blocks-authelia-options-shb.authelia.extraOidcClaimsPolicies": [
|
||||
"blocks-authelia.html#blocks-authelia-options-shb.authelia.extraOidcClaimsPolicies"
|
||||
],
|
||||
"blocks-authelia-options-shb.authelia.extraOidcScopes": [
|
||||
"blocks-authelia.html#blocks-authelia-options-shb.authelia.extraOidcScopes"
|
||||
],
|
||||
"blocks-authelia-options-shb.authelia.ldapHostname": [
|
||||
"blocks-authelia.html#blocks-authelia-options-shb.authelia.ldapHostname"
|
||||
],
|
||||
|
|
@ -3041,6 +3050,9 @@
|
|||
"services-nextcloudserver-options-shb.nextcloud.apps.sso": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.sso.adminGroup": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.adminGroup"
|
||||
],
|
||||
"services-nextcloudserver-options-shb.nextcloud.apps.sso.authorization_policy": [
|
||||
"services-nextcloud.html#services-nextcloudserver-options-shb.nextcloud.apps.sso.authorization_policy"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -140,6 +140,24 @@ in
|
|||
};
|
||||
};
|
||||
|
||||
extraOidcClaimsPolicies = lib.mkOption {
|
||||
description = "Extra OIDC claims policies.";
|
||||
type = lib.types.attrsOf lib.types.attrs;
|
||||
default = {};
|
||||
};
|
||||
|
||||
extraOidcScopes = lib.mkOption {
|
||||
description = "Extra OIDC scopes.";
|
||||
type = lib.types.attrsOf lib.types.attrs;
|
||||
default = {};
|
||||
};
|
||||
|
||||
extraDefinitions = lib.mkOption {
|
||||
description = "Extra definitions.";
|
||||
type = lib.types.attrsOf lib.types.attrs;
|
||||
default = {};
|
||||
};
|
||||
|
||||
oidcClients = lib.mkOption {
|
||||
description = "OIDC clients";
|
||||
default = [
|
||||
|
|
@ -205,13 +223,13 @@ in
|
|||
|
||||
scopes = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
description = "Scopes to ask for";
|
||||
description = "Scopes to ask for. See https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims";
|
||||
example = [ "openid" "profile" "email" "groups" ];
|
||||
default = [];
|
||||
};
|
||||
|
||||
claims_policy = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
description = ''
|
||||
Claim policy.
|
||||
|
||||
|
|
@ -387,11 +405,6 @@ in
|
|||
user = "uid=admin,ou=people,${cfg.dcdomain}";
|
||||
};
|
||||
};
|
||||
# This should go away at some point.
|
||||
# https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/#restore-functionality-prior-to-claims-parameter
|
||||
identity_providers.oidc = {
|
||||
claims_policies.default.id_token = [ "email" "preferred_username" "name" "groups" ];
|
||||
};
|
||||
totp = {
|
||||
disable = "false";
|
||||
issuer = fqdnWithPort;
|
||||
|
|
@ -465,6 +478,17 @@ in
|
|||
};
|
||||
|
||||
log.level = if cfg.debug then "debug" else "info";
|
||||
} // {
|
||||
identity_providers.oidc = {
|
||||
claims_policies = {
|
||||
# This default claim should go away at some point.
|
||||
# https://www.authelia.com/integration/openid-connect/openid-connect-1.0-claims/#restore-functionality-prior-to-claims-parameter
|
||||
default.id_token = [ "email" "preferred_username" "name" "groups" ];
|
||||
} // cfg.extraOidcClaimsPolicies;
|
||||
scopes = cfg.extraOidcScopes;
|
||||
};
|
||||
} // lib.optionalAttrs (cfg.extraDefinitions != {}) {
|
||||
definitions = cfg.extraDefinitions;
|
||||
};
|
||||
|
||||
settingsFiles = [ "/var/lib/authelia-${fqdn}/oidc_clients.yaml" ];
|
||||
|
|
|
|||
|
|
@ -414,7 +414,6 @@ in
|
|||
};
|
||||
};
|
||||
|
||||
|
||||
userGroup = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Group users must belong to to be able to login to Nextcloud.";
|
||||
|
|
@ -424,8 +423,8 @@ in
|
|||
configID = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
description = ''
|
||||
Multiple LDAP configs can co-exist with only one active at a time.This option
|
||||
sets the config ID used by Self Host Blocks.
|
||||
Multiple LDAP configs can co-exist with only one active at a time.
|
||||
This option sets the config ID used by Self Host Blocks.
|
||||
'';
|
||||
default = 50;
|
||||
};
|
||||
|
|
@ -475,6 +474,12 @@ in
|
|||
default = "one_factor";
|
||||
};
|
||||
|
||||
adminGroup = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Group admins must belong to to be able to login to Nextcloud.";
|
||||
default = "nextcloud_admin";
|
||||
};
|
||||
|
||||
secret = lib.mkOption {
|
||||
description = "OIDC shared secret.";
|
||||
type = lib.types.submodule {
|
||||
|
|
@ -1022,7 +1027,7 @@ in
|
|||
${occ} ldap:set-config "${cID}" 'ldapLoginFilterAttributes' \
|
||||
'mail;objectclass'
|
||||
${occ} ldap:set-config "${cID}" 'ldapUserDisplayName' \
|
||||
'displayname'
|
||||
'givenname'
|
||||
${occ} ldap:set-config "${cID}" 'ldapUserFilter' \
|
||||
'(&(objectclass=person)(memberOf=cn=${cfg'.userGroup},ou=groups,${cfg'.dcdomain}))'
|
||||
${occ} ldap:set-config "${cID}" 'ldapUserFilterMode' \
|
||||
|
|
@ -1033,7 +1038,7 @@ in
|
|||
# Nextcloud is compatible with the one returned by a (possibly added in the future) SSO
|
||||
# provider.
|
||||
${occ} ldap:set-config "${cID}" 'ldapExpertUsernameAttr' \
|
||||
'user_id'
|
||||
'uid'
|
||||
|
||||
${occ} ldap:test-config -- "${cID}"
|
||||
|
||||
|
|
@ -1057,6 +1062,7 @@ in
|
|||
"profile"
|
||||
"email"
|
||||
"groups"
|
||||
"nextcloud_userinfo"
|
||||
];
|
||||
in lib.mkIf (cfg.enable && cfg.apps.sso.enable) {
|
||||
assertions = [
|
||||
|
|
@ -1070,11 +1076,9 @@ in
|
|||
}
|
||||
];
|
||||
|
||||
systemd.services.nextcloud-setup.script =
|
||||
''
|
||||
${occ} app:install oidc_login || :
|
||||
${occ} app:enable oidc_login
|
||||
'';
|
||||
services.nextcloud.extraApps = {
|
||||
inherit (nextcloudApps) oidc_login;
|
||||
};
|
||||
|
||||
systemd.services.nextcloud-setup-pre = {
|
||||
wantedBy = [ "multi-user.target" ];
|
||||
|
|
@ -1096,6 +1100,12 @@ in
|
|||
secretFile = "${cfg.dataDir}/config/secretFile";
|
||||
|
||||
# See all options at https://github.com/pulsejet/nextcloud-oidc-login
|
||||
# Other important url/links are:
|
||||
# ${fqdn}/.well-known/openid-configuration
|
||||
# https://www.authelia.com/reference/guides/attributes/#custom-attributes
|
||||
# https://github.com/lldap/lldap/blob/main/example_configs/nextcloud_oidc_authelia.md
|
||||
# https://www.authelia.com/integration/openid-connect/nextcloud/#authelia
|
||||
# https://www.openidconnect.net/
|
||||
settings = {
|
||||
allow_user_to_change_display_name = false;
|
||||
lost_password_link = "disabled";
|
||||
|
|
@ -1110,12 +1120,14 @@ in
|
|||
oidc_login_logout_url = ssoFqdnWithPort;
|
||||
oidc_login_button_text = "Log in with ${cfg.apps.sso.provider}";
|
||||
oidc_login_hide_password_form = false;
|
||||
oidc_login_use_id_token = true;
|
||||
# Now, Authelia provides the info using the UserInfo request.
|
||||
oidc_login_use_id_token = false;
|
||||
oidc_login_attributes = {
|
||||
id = "preferred_username";
|
||||
name = "name";
|
||||
mail = "email";
|
||||
groups = "groups";
|
||||
is_admin = "is_nextcloud_admin";
|
||||
};
|
||||
oidc_login_default_group = "oidc";
|
||||
oidc_login_use_external_storage = false;
|
||||
|
|
@ -1136,8 +1148,7 @@ in
|
|||
# they are not already existing, Default is `false`. This creates groups for all groups
|
||||
# the user is associated with in LDAP. It's too much.
|
||||
oidc_create_groups = false;
|
||||
# Enable use of WebDAV via OIDC bearer token.
|
||||
oidc_login_webdav_enabled = true;
|
||||
oidc_login_webdav_enabled = false;
|
||||
oidc_login_password_authentication = false;
|
||||
oidc_login_public_key_caching_time = 86400;
|
||||
oidc_login_min_time_between_jwks_requests = 10;
|
||||
|
|
@ -1145,19 +1156,39 @@ in
|
|||
# If true, nextcloud will download user avatars on login. This may lead to security issues
|
||||
# as the server does not control which URLs will be requested. Use with care.
|
||||
oidc_login_update_avatar = false;
|
||||
oidc_login_code_challenge_method = "S256";
|
||||
};
|
||||
};
|
||||
|
||||
shb.authelia.extraDefinitions = {
|
||||
user_attributes."is_nextcloud_admin".expression = ''type(groups) == list && "${cfg.apps.sso.adminGroup}" in groups'';
|
||||
};
|
||||
shb.authelia.extraOidcClaimsPolicies."nextcloud_userinfo" = {
|
||||
custom_claims = {
|
||||
is_nextcloud_admin = {};
|
||||
};
|
||||
};
|
||||
shb.authelia.extraOidcScopes."nextcloud_userinfo" = {
|
||||
claims = [ "is_nextcloud_admin" ];
|
||||
};
|
||||
|
||||
shb.authelia.oidcClients = lib.mkIf (cfg.apps.sso.provider == "Authelia") [
|
||||
{
|
||||
client_id = cfg.apps.sso.clientID;
|
||||
client_name = "Nextcloud";
|
||||
client_secret.source = cfg.apps.sso.secretForAuthelia.result.path;
|
||||
claims_policy = "nextcloud_userinfo";
|
||||
public = false;
|
||||
authorization_policy = cfg.apps.sso.authorization_policy;
|
||||
require_pkce = "true";
|
||||
pkce_challenge_method = "S256";
|
||||
redirect_uris = [ "${protocol}://${fqdnWithPort}/apps/oidc_login/oidc" ];
|
||||
inherit scopes;
|
||||
userinfo_signing_algorithm = "none";
|
||||
response_types = [ "code" ];
|
||||
grant_types = [ "authorization_code" ];
|
||||
access_token_signed_response_alg = "none";
|
||||
userinfo_signed_response_alg = "none";
|
||||
token_endpoint_auth_method = "client_secret_basic";
|
||||
}
|
||||
];
|
||||
})
|
||||
|
|
|
|||
|
|
@ -179,10 +179,9 @@ to do that with SOPS we use the `key` option so that both
|
|||
and `sops.secrets."nextcloud/ldapUserPassword"`
|
||||
secrets have the same content.
|
||||
|
||||
Creating LDAP users and groups is not declarative yet,
|
||||
so go to the LDAP server at `http://ldap.example.com`,
|
||||
create the `nextcloud_user` group,
|
||||
create a user and add it to the group.
|
||||
The LDAP [user group](#services-nextcloudserver-options-shb.nextcloud.apps.ldap.userGroup) is created automatically.
|
||||
Add your user to it by going to `http://ldap.example.com`,
|
||||
create a user if needed and add it to the group.
|
||||
When that's done, go back to the Nextcloud server at
|
||||
`https://nextcloud.example.com` and login with that user.
|
||||
|
||||
|
|
@ -190,6 +189,7 @@ Note that we cannot create an admin user from the LDAP server,
|
|||
so you need to create a normal user like above,
|
||||
login with it once so it is known to Nextcloud, then logout,
|
||||
login with the admin Nextcloud user and promote that new user to admin level.
|
||||
This limitation does not exist with the [SSO integration](#services-nextcloudserver-usage-oidc).
|
||||
|
||||
### With SSO Support {#services-nextcloudserver-usage-oidc}
|
||||
|
||||
|
|
@ -231,6 +231,12 @@ to do that with SOPS we use the `key` option so that both
|
|||
and `sops.secrets."nextcloud/sso/secretForAuthelia"`
|
||||
secrets have the same content.
|
||||
|
||||
The LDAP [user group](#services-nextcloudserver-options-shb.nextcloud.apps.ldap.userGroup) and [admin group](#services-nextcloudserver-options-shb.nextcloud.apps.sso.adminGroup) are created automatically.
|
||||
Add your user to one or both by going to `http://ldap.example.com`,
|
||||
create a user if needed and add it to the groups.
|
||||
When that's done, go back to the Nextcloud server at
|
||||
`https://nextcloud.example.com` and login with that user.
|
||||
|
||||
Setting the `fallbackDefaultAuth` to `false` means the only way to login is through Authelia.
|
||||
If this does not work for any reason, you can let users login through Nextcloud directly by setting this option to `true`.
|
||||
|
||||
|
|
|
|||
|
|
@ -198,6 +198,10 @@ in
|
|||
# HTTP(s) server port.
|
||||
networking.firewall.allowedTCPPorts = [ 80 443 ];
|
||||
shb.nginx.accessLog = true;
|
||||
|
||||
networking.hosts = {
|
||||
"192.168.1.2" = [ config.test.fqdn ];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -415,6 +419,11 @@ in
|
|||
groups = [ "user_group" "admin_group" ];
|
||||
password.result.path = pkgs.writeText "bobPassword" "BobPassword";
|
||||
};
|
||||
# charlie = {
|
||||
# email = "charlie@example.com";
|
||||
# groups = [ ];
|
||||
# password.result.path = pkgs.writeText "charliePassword" "CharliePassword";
|
||||
# };
|
||||
};
|
||||
|
||||
ensureGroups = {
|
||||
|
|
@ -430,7 +439,7 @@ in
|
|||
];
|
||||
|
||||
networking.hosts = {
|
||||
"127.0.0.1" = [ "auth.${config.test.domain}" ];
|
||||
"127.0.0.1" = [ "${config.shb.authelia.subdomain}.${config.shb.authelia.domain}" ];
|
||||
};
|
||||
|
||||
shb.authelia = {
|
||||
|
|
|
|||
|
|
@ -151,6 +151,91 @@ let
|
|||
};
|
||||
};
|
||||
|
||||
clientLdapLogin = { config, ... }: {
|
||||
imports = [
|
||||
testLib.baseModule
|
||||
testLib.clientLoginModule
|
||||
];
|
||||
virtualisation.memorySize = 4096;
|
||||
|
||||
test = {
|
||||
subdomain = "n";
|
||||
};
|
||||
|
||||
test.login = {
|
||||
startUrl = "http://${config.test.fqdn}";
|
||||
usernameFieldLabelRegex = "Account name";
|
||||
passwordFieldLabelRegex = "^ *[Pp]assword";
|
||||
loginButtonNameRegex = "[Ll]og [Ii]n";
|
||||
testLoginWith = [
|
||||
{ username = "alice"; password = "AlicePassword"; nextPageExpect = [
|
||||
"expect(page.get_by_text('Wrong login or password')).not_to_be_visible()"
|
||||
"expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()"
|
||||
"expect(page).to_have_title(re.compile('Dashboard'))"
|
||||
]; }
|
||||
{ username = "alice"; password = "NotAlicePassword"; nextPageExpect = [
|
||||
"expect(page.get_by_text('Wrong login or password')).to_be_visible()"
|
||||
]; }
|
||||
{ username = "bob"; password = "BobPassword"; nextPageExpect = [
|
||||
"expect(page.get_by_text('Wrong login or password')).not_to_be_visible()"
|
||||
"expect(page.get_by_role('button', name=re.compile('[Ll]og [Ii]n'))).not_to_be_visible()"
|
||||
"expect(page).to_have_title(re.compile('Dashboard'))"
|
||||
]; }
|
||||
{ username = "bob"; password = "NotBobPassword"; nextPageExpect = [
|
||||
"expect(page.get_by_text('Wrong login or password')).to_be_visible()"
|
||||
]; }
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
clientSsoLogin = { config, ... }: {
|
||||
imports = [
|
||||
testLib.baseModule
|
||||
testLib.clientLoginModule
|
||||
];
|
||||
virtualisation.memorySize = 4096;
|
||||
|
||||
test = {
|
||||
subdomain = "n";
|
||||
};
|
||||
|
||||
networking.hosts = {
|
||||
"192.168.1.2" = [ "auth.example.com" ];
|
||||
};
|
||||
|
||||
test.login = {
|
||||
startUrl = "http://${config.test.fqdn}";
|
||||
usernameFieldLabelRegex = "Username";
|
||||
passwordFieldSelector = "get_by_label(\"Password *\")";
|
||||
loginButtonNameRegex = "[sS]ign [iI]n";
|
||||
testLoginWith = [
|
||||
{ username = "alice"; password = "AlicePassword"; nextPageExpect = [
|
||||
"page.get_by_role('button', name=re.compile('Accept')).click()"
|
||||
"expect(page).to_have_title(re.compile('Dashboard'))"
|
||||
"page.goto('https://${config.test.fqdn}/settings/admin')"
|
||||
"expect(page.get_by_text('Access forbidden')).to_be_visible()"
|
||||
]; }
|
||||
{ username = "alice"; password = "NotAlicePassword"; nextPageExpect = [
|
||||
"expect(page.get_by_text('Incorrect username or password')).to_be_visible()"
|
||||
]; }
|
||||
{ username = "bob"; password = "BobPassword"; nextPageExpect = [
|
||||
"page.get_by_role('button', name=re.compile('Accept')).click()"
|
||||
"expect(page).to_have_title(re.compile('Dashboard'))"
|
||||
"page.goto('https://${config.test.fqdn}/settings/admin')"
|
||||
"expect(page.get_by_text('Access forbidden')).not_to_be_visible()"
|
||||
]; }
|
||||
{ username = "bob"; password = "NotBobPassword"; nextPageExpect = [
|
||||
"expect(page.get_by_text('Incorrect username or password')).to_be_visible()"
|
||||
]; }
|
||||
# TODO: charlie has no groups, which makes lldap return a 'null' value instead of an empty array and Authelia does not like that.
|
||||
# { username = "charlie"; password = "CharliePassword"; nextPageExpect = [
|
||||
# "page.get_by_role('button', name=re.compile('Accept')).click()"
|
||||
# "expect(page).to_have_title(re.compile('Dashboard'))"
|
||||
# ]; }
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
https = { config, ...}: {
|
||||
shb.nextcloud = {
|
||||
ssl = config.shb.certs.certs.selfsigned.n;
|
||||
|
|
@ -168,7 +253,7 @@ let
|
|||
dcdomain = config.shb.lldap.dcdomain;
|
||||
adminName = "admin";
|
||||
adminPassword.result = config.shb.hardcodedsecret.nextcloudLdapUserPassword.result;
|
||||
userGroup = "nextcloud_user";
|
||||
userGroup = "user_group";
|
||||
};
|
||||
};
|
||||
shb.hardcodedsecret.nextcloudLdapUserPassword = {
|
||||
|
|
@ -184,7 +269,7 @@ let
|
|||
enable = true;
|
||||
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
|
||||
clientID = "nextcloud";
|
||||
# adminUserGroup = "nextcloud_admin";
|
||||
adminGroup = "admin_group";
|
||||
|
||||
secret.result = config.shb.hardcodedsecret.oidcSecret.result;
|
||||
secretForAuthelia.result = config.shb.hardcodedsecret.oidcAutheliaSecret.result;
|
||||
|
|
@ -192,6 +277,10 @@ let
|
|||
fallbackDefaultAuth = false;
|
||||
};
|
||||
};
|
||||
# Needed because OIDC somehow does not like self-signed certificates
|
||||
# which we do use in tests.
|
||||
# See https://github.com/pulsejet/nextcloud-oidc-login/issues/267
|
||||
services.nextcloud.settings.oidc_login_tls_verify = lib.mkForce false;
|
||||
|
||||
shb.hardcodedsecret.oidcSecret = {
|
||||
request = config.shb.nextcloud.apps.sso.secret.request;
|
||||
|
|
@ -436,13 +525,19 @@ in
|
|||
];
|
||||
};
|
||||
|
||||
nodes.client = {};
|
||||
nodes.client = {
|
||||
imports = [
|
||||
clientLdapLogin
|
||||
];
|
||||
};
|
||||
|
||||
testScript = commonTestScript.access;
|
||||
};
|
||||
|
||||
sso = pkgs.testers.runNixOSTest {
|
||||
name = "nextcloud_sso";
|
||||
|
||||
interactive.sshBackdoor.enable = true;
|
||||
|
||||
nodes.server = { config, ... }: {
|
||||
imports = [
|
||||
|
|
@ -453,10 +548,24 @@ in
|
|||
ldap
|
||||
(testLib.sso config.shb.certs.certs.selfsigned.n)
|
||||
sso
|
||||
({ config, ... }: {
|
||||
networking.hosts = {
|
||||
"127.0.0.1" = [ config.test.fqdn ];
|
||||
};
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
nodes.client = {};
|
||||
nodes.client = {
|
||||
imports = [
|
||||
clientSsoLogin
|
||||
({ config, ... }: {
|
||||
networking.hosts = {
|
||||
"192.168.1.2" = [ config.test.fqdn ];
|
||||
};
|
||||
})
|
||||
];
|
||||
};
|
||||
|
||||
testScript = commonTestScript.access;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue