1
0
Fork 0

add vm tests for jellyfin and regroup ldap and sso options

This commit is contained in:
ibizaman 2024-03-02 22:58:36 -08:00 committed by Pierre Penninckx
parent 97f213a137
commit e80cc0d3aa
4 changed files with 439 additions and 78 deletions

View file

@ -100,6 +100,7 @@
};
}
// (vm_test "authelia" ./test/vm/authelia.nix)
// (vm_test "jellyfin" ./test/vm/jellyfin.nix)
// (vm_test "ldap" ./test/vm/ldap.nix)
// (vm_test "lib" ./test/vm/lib.nix)
// (vm_test "postgresql" ./test/vm/postgresql.nix)

View file

@ -22,9 +22,9 @@ rec {
''
set -euo pipefail
set -x
mkdir -p $(dirname ${templatePath})
ln -fs ${file} ${templatePath}
rm -f ${resultPath}
${pkgs.gnused}/bin/sed ${sedPatterns} ${templatePath}
${pkgs.gnused}/bin/sed ${sedPatterns} ${templatePath} > ${resultPath}
'';

View file

@ -30,62 +30,94 @@ in
default = null;
};
ldapHost = lib.mkOption {
ldap = lib.mkOption {
description = "LDAP configuration.";
default = {};
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "LDAP";
host = lib.mkOption {
type = lib.types.str;
description = "host serving the LDAP server";
description = "Host serving the LDAP server.";
example = "127.0.0.1";
};
ldapPort = lib.mkOption {
port = lib.mkOption {
type = lib.types.int;
description = "port where the LDAP server is listening";
description = "Port where the LDAP server is listening.";
example = 389;
};
dcdomain = lib.mkOption {
type = lib.types.str;
description = "dc domain for ldap";
description = "DC domain for LDAP.";
example = "dc=mydomain,dc=com";
};
oidcProvider = lib.mkOption {
userGroup = lib.mkOption {
type = lib.types.str;
description = "LDAP user group";
default = "jellyfin_user";
};
adminGroup = lib.mkOption {
type = lib.types.str;
description = "LDAP admin group";
default = "jellyfin_admin";
};
passwordFile = lib.mkOption {
type = lib.types.path;
description = "File containing the LDAP admin password.";
};
};
};
};
sso = lib.mkOption {
description = "SSO configuration.";
default = {};
type = lib.types.submodule {
options = {
enable = lib.mkEnableOption "SSO";
provider = lib.mkOption {
type = lib.types.str;
description = "OIDC provider name";
default = "Authelia";
};
authEndpoint = lib.mkOption {
endpoint = lib.mkOption {
type = lib.types.str;
description = "OIDC endpoint for SSO";
example = "https://authelia.example.com";
};
oidcClientID = lib.mkOption {
clientID = lib.mkOption {
type = lib.types.str;
description = "Client ID for the OIDC endpoint";
default = "jellyfin";
};
oidcAdminUserGroup = lib.mkOption {
adminUserGroup = lib.mkOption {
type = lib.types.str;
description = "OIDC admin group";
default = "jellyfin_admin";
};
oidcUserGroup = lib.mkOption {
userGroup = lib.mkOption {
type = lib.types.str;
description = "OIDC user group";
default = "jellyfin_user";
};
ldapPasswordFile = lib.mkOption {
secretFile = lib.mkOption {
type = lib.types.path;
description = "File containing the LDAP admin password.";
description = "File containing the OIDC shared secret.";
};
};
};
ssoSecretFile = lib.mkOption {
type = lib.types.path;
description = "File containing the SSO shared secret.";
};
};
@ -107,6 +139,8 @@ in
};
};
services.nginx.enable = true;
# Take advice from https://jellyfin.org/docs/general/networking/nginx/ and https://nixos.wiki/wiki/Plex
services.nginx.virtualHosts."${fqdn}" = {
forceSSL = !(isNull cfg.ssl);
@ -238,17 +272,17 @@ in
ldapConfig = pkgs.writeText "LDAP-Auth.xml" ''
<?xml version="1.0" encoding="utf-8"?>
<PluginConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<LdapServer>${cfg.ldapHost}</LdapServer>
<LdapPort>${builtins.toString cfg.ldapPort}</LdapPort>
<LdapServer>${cfg.ldap.host}</LdapServer>
<LdapPort>${builtins.toString cfg.ldap.port}</LdapPort>
<UseSsl>false</UseSsl>
<UseStartTls>false</UseStartTls>
<SkipSslVerify>false</SkipSslVerify>
<LdapBindUser>uid=admin,ou=people,${cfg.dcdomain}</LdapBindUser>
<LdapBindUser>uid=admin,ou=people,${cfg.ldap.dcdomain}</LdapBindUser>
<LdapBindPassword>%LDAP_PASSWORD%</LdapBindPassword>
<LdapBaseDn>ou=people,${cfg.dcdomain}</LdapBaseDn>
<LdapSearchFilter>(memberof=cn=jellyfin_user,ou=groups,${cfg.dcdomain})</LdapSearchFilter>
<LdapAdminBaseDn>ou=people,${cfg.dcdomain}</LdapAdminBaseDn>
<LdapAdminFilter>(memberof=cn=jellyfin_admin,ou=groups,${cfg.dcdomain})</LdapAdminFilter>
<LdapBaseDn>ou=people,${cfg.ldap.dcdomain}</LdapBaseDn>
<LdapSearchFilter>(memberof=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain})</LdapSearchFilter>
<LdapAdminBaseDn>ou=people,${cfg.ldap.dcdomain}</LdapAdminBaseDn>
<LdapAdminFilter>(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})</LdapAdminFilter>
<EnableLdapAdminFilterMemberUid>false</EnableLdapAdminFilterMemberUid>
<LdapSearchAttributes>uid, cn, mail, displayName</LdapSearchAttributes>
<LdapClientCertPath />
@ -271,22 +305,22 @@ in
<OidConfigs>
<item>
<key>
<string>${cfg.oidcProvider}</string>
<string>${cfg.sso.provider}</string>
</key>
<value>
<PluginConfiguration>
<OidEndpoint>${cfg.authEndpoint}</OidEndpoint>
<OidClientId>${cfg.oidcClientID}</OidClientId>
<OidEndpoint>${cfg.sso.endpoint}</OidEndpoint>
<OidClientId>${cfg.sso.clientID}</OidClientId>
<OidSecret>%SSO_SECRET%</OidSecret>
<Enabled>true</Enabled>
<EnableAuthorization>true</EnableAuthorization>
<EnableAllFolders>true</EnableAllFolders>
<EnabledFolders />
<AdminRoles>
<string>${cfg.oidcAdminUserGroup}</string>
<string>${cfg.sso.adminUserGroup}</string>
</AdminRoles>
<Roles>
<string>${cfg.oidcUserGroup}</string>
<string>${cfg.sso.userGroup}</string>
</Roles>
<EnableFolderRoles>false</EnableFolderRoles>
<FolderRoleMappings />
@ -305,15 +339,15 @@ in
brandingConfig = pkgs.writeText "branding.xml" ''
<?xml version="1.0" encoding="utf-8"?>
<BrandingOptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<LoginDisclaimer>&lt;a href="https://${cfg.subdomain}.${cfg.domain}/SSO/OID/p/${cfg.oidcProvider}" class="raised cancel block emby-button authentik-sso"&gt;
Sign in with ${cfg.oidcProvider}&amp;nbsp;
<LoginDisclaimer>&lt;a href="https://${cfg.subdomain}.${cfg.domain}/SSO/OID/p/${cfg.sso.provider}" class="raised cancel block emby-button authentik-sso"&gt;
Sign in with ${cfg.sso.provider}&amp;nbsp;
&lt;img alt="OpenID Connect (authentik)" title="OpenID Connect (authentik)" class="oauth-login-image" src="https://raw.githubusercontent.com/goauthentik/authentik/master/web/icons/icon.png"&gt;
&lt;/a&gt;
&lt;a href="https://${cfg.subdomain}.${cfg.domain}/SSOViews/linking" class="raised cancel block emby-button authentik-sso"&gt;
Link ${cfg.oidcProvider} config&amp;nbsp;
Link ${cfg.sso.provider} config&amp;nbsp;
&lt;/a&gt;
&lt;a href="${cfg.authEndpoint}" class="raised cancel block emby-button authentik-sso"&gt;
${cfg.oidcProvider} config&amp;nbsp;
&lt;a href="${cfg.sso.endpoint}" class="raised cancel block emby-button authentik-sso"&gt;
${cfg.sso.provider} config&amp;nbsp;
&lt;/a&gt;
</LoginDisclaimer>
<CustomCss>
@ -348,36 +382,36 @@ in
</BrandingOptions>
'';
in
shblib.replaceSecretsScript {
lib.strings.optionalString cfg.ldap.enable (shblib.replaceSecretsScript {
file = ldapConfig;
resultPath = "/var/lib/jellyfin/plugins/configurations/LDAP-Auth.xml";
replacements = {
"%LDAP_PASSWORD%" = "$(cat ${cfg.ldapPasswordFile})";
"%LDAP_PASSWORD%" = "$(cat ${cfg.ldap.passwordFile})";
};
}
+ shblib.replaceSecretsScript {
})
+ lib.strings.optionalString cfg.sso.enable (shblib.replaceSecretsScript {
file = ssoConfig;
resultPath = "/var/lib/jellyfin/plugins/configurations/SSO-Auth.xml";
replacements = {
"%SSO_SECRET%" = "$(cat ${cfg.ssoSecretFile})";
"%SSO_SECRET%" = "$(cat ${cfg.sso.secretFile})";
};
}
+ shblib.replaceSecretsScript {
})
+ lib.strings.optionalString cfg.sso.enable (shblib.replaceSecretsScript {
file = brandingConfig;
resultPath = "/var/lib/jellyfin/config/branding.xml";
replacements = {
"%a%" = "%a%";
};
};
});
shb.authelia.oidcClients = [
shb.authelia.oidcClients = lib.lists.optionals (!(isNull cfg.sso)) [
{
id = cfg.oidcClientID;
id = cfg.sso.clientID;
description = "Jellyfin";
secret.source = cfg.ssoSecretFile;
secret.source = cfg.sso.secretFile;
public = false;
authorization_policy = "one_factor";
redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/sso/OID/r/${cfg.oidcProvider}" ];
redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/sso/OID/r/${cfg.sso.provider}" ];
}
];

326
test/vm/jellyfin.nix Normal file
View file

@ -0,0 +1,326 @@
{ pkgs, lib, ... }:
{
basic = pkgs.nixosTest {
name = "jellyfin-basic";
nodes.server = { config, pkgs, ... }: {
imports = [
{
options = {
shb.backup = lib.mkOption { type = lib.types.anything; };
shb.authelia = lib.mkOption { type = lib.types.anything; };
};
}
../../modules/services/jellyfin.nix
];
shb.jellyfin = {
enable = true;
domain = "example.com";
subdomain = "j";
};
# Nginx port.
networking.firewall.allowedTCPPorts = [ 80 ];
};
nodes.client = {};
# TODO: Test login
testScript = { nodes, ... }: ''
import json
def curl(target, format, endpoint):
return json.loads(target.succeed(
"curl --fail-with-body --silent --show-error --output /dev/null --location"
+ " --connect-to j.example.com:443:server:443"
+ " --connect-to j.example.com:80:server:80"
+ f" --write-out '{format}'"
+ " " + endpoint
))
start_all()
server.wait_for_unit("jellyfin.service")
server.wait_for_unit("nginx.service")
server.wait_for_open_port(8096)
response = curl(client, """{"code":%{response_code}}""", "http://j.example.com")
if response['code'] != 200:
raise Exception(f"Code is {response['code']}")
'';
};
ldap = pkgs.nixosTest {
name = "jellyfin-ldap";
nodes.server = { config, pkgs, ... }: {
imports = [
{
options = {
shb.backup = lib.mkOption { type = lib.types.anything; };
shb.authelia = lib.mkOption { type = lib.types.anything; };
};
}
../../modules/blocks/ldap.nix
../../modules/services/jellyfin.nix
];
shb.ldap = {
enable = true;
domain = "example.com";
subdomain = "ldap";
ldapPort = 3890;
webUIListenPort = 17170;
dcdomain = "dc=example,dc=com";
ldapUserPasswordFile = pkgs.writeText "ldapUserPassword" "ldapUserPassword";
jwtSecretFile = pkgs.writeText "jwtSecret" "jwtSecret";
};
shb.jellyfin = {
enable = true;
domain = "example.com";
subdomain = "j";
ldap = {
enable = true;
host = "127.0.0.1";
port = config.shb.ldap.ldapPort;
dcdomain = config.shb.ldap.dcdomain;
passwordFile = pkgs.writeText "ldapUserPassword" "ldapUserPassword";
};
};
# Nginx port.
networking.firewall.allowedTCPPorts = [ 80 ];
};
nodes.client = {};
# TODO: Test login with ldap user
testScript = { nodes, ... }: ''
import json
def curl(target, format, endpoint):
return json.loads(target.succeed(
"curl --fail-with-body --silent --show-error --output /dev/null --location"
+ " --connect-to j.example.com:443:server:443"
+ " --connect-to j.example.com:80:server:80"
+ f" --write-out '{format}'"
+ " " + endpoint
))
start_all()
server.wait_for_unit("jellyfin.service")
server.wait_for_unit("nginx.service")
server.wait_for_unit("lldap.service")
server.wait_for_open_port(8096)
response = curl(client, """{"code":%{response_code}}""", "http://j.example.com")
if response['code'] != 200:
raise Exception(f"Code is {response['code']}")
'';
};
cert = pkgs.nixosTest {
name = "jellyfin_cert";
nodes.server = { config, pkgs, ... }: {
imports = [
{
options = {
shb.backup = lib.mkOption { type = lib.types.anything; };
shb.authelia = lib.mkOption { type = lib.types.anything; };
};
}
../../modules/blocks/nginx.nix
../../modules/blocks/postgresql.nix
../../modules/blocks/ssl.nix
../../modules/services/jellyfin.nix
];
shb.certs = {
cas.selfsigned.myca = {
name = "My CA";
};
certs.selfsigned = {
n = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "*.example.com";
group = "nginx";
};
};
};
systemd.services.nginx.after = [ config.shb.certs.certs.selfsigned.n.systemdService ];
systemd.services.nginx.requires = [ config.shb.certs.certs.selfsigned.n.systemdService ];
shb.jellyfin = {
enable = true;
domain = "example.com";
subdomain = "j";
ssl = config.shb.certs.certs.selfsigned.n;
};
# Nginx port.
networking.firewall.allowedTCPPorts = [ 80 443 ];
shb.nginx.accessLog = true;
};
nodes.client = {};
# TODO: Test login
testScript = { nodes, ... }: ''
import json
import os
import pathlib
def curl(target, format, endpoint):
return json.loads(target.succeed(
"curl --fail-with-body --silent --show-error --output /dev/null --location"
+ " --connect-to j.example.com:443:server:443"
+ " --connect-to j.example.com:80:server:80"
+ f" --write-out '{format}'"
+ " " + endpoint
))
start_all()
server.wait_for_unit("jellyfin.service")
server.wait_for_unit("nginx.service")
server.wait_for_open_port(8096)
server.copy_from_vm("/etc/ssl/certs/ca-certificates.crt")
client.succeed("rm -r /etc/ssl/certs")
client.copy_from_host(str(pathlib.Path(os.environ.get("out", os.getcwd())) / "ca-certificates.crt"), "/etc/ssl/certs/ca-certificates.crt")
response = curl(client, """{"code":%{response_code}}""", "https://j.example.com")
if response['code'] != 200:
raise Exception(f"Code is {response['code']}")
'';
};
sso = pkgs.nixosTest {
name = "jellyfin_sso";
nodes.server = { config, pkgs, ... }: {
imports = [
{
options = {
shb.backup = lib.mkOption { type = lib.types.anything; };
};
}
../../modules/blocks/authelia.nix
../../modules/blocks/ldap.nix
../../modules/blocks/postgresql.nix
../../modules/blocks/ssl.nix
../../modules/services/jellyfin.nix
];
shb.ldap = {
enable = true;
domain = "example.com";
subdomain = "ldap";
ldapPort = 3890;
webUIListenPort = 17170;
dcdomain = "dc=example,dc=com";
ldapUserPasswordFile = pkgs.writeText "ldapUserPassword" "ldapUserPassword";
jwtSecretFile = pkgs.writeText "jwtSecret" "jwtSecret";
};
shb.certs = {
cas.selfsigned.myca = {
name = "My CA";
};
certs.selfsigned = {
n = {
ca = config.shb.certs.cas.selfsigned.myca;
domain = "*.example.com";
group = "nginx";
};
};
};
systemd.services.nginx.after = [ config.shb.certs.certs.selfsigned.n.systemdService ];
systemd.services.nginx.requires = [ config.shb.certs.certs.selfsigned.n.systemdService ];
shb.authelia = {
enable = true;
domain = "example.com";
subdomain = "auth";
ssl = config.shb.certs.certs.selfsigned.n;
ldapEndpoint = "ldap://127.0.0.1:${builtins.toString config.shb.ldap.ldapPort}";
dcdomain = config.shb.ldap.dcdomain;
secrets = {
jwtSecretFile = pkgs.writeText "jwtSecret" "jwtSecret";
ldapAdminPasswordFile = pkgs.writeText "ldapUserPassword" "ldapUserPassword";
sessionSecretFile = pkgs.writeText "sessionSecret" "sessionSecret";
storageEncryptionKeyFile = pkgs.writeText "storageEncryptionKey" "storageEncryptionKey";
identityProvidersOIDCHMACSecretFile = pkgs.writeText "identityProvidersOIDCHMACSecret" "identityProvidersOIDCHMACSecret";
identityProvidersOIDCIssuerPrivateKeyFile = (pkgs.runCommand "gen-private-key" {} ''
mkdir $out
${pkgs.openssl}/bin/openssl genrsa -out $out/private.pem 4096
'') + "/private.pem";
};
};
shb.jellyfin = {
enable = true;
domain = "example.com";
subdomain = "j";
ssl = config.shb.certs.certs.selfsigned.n;
ldap = {
enable = true;
host = "127.0.0.1";
port = config.shb.ldap.ldapPort;
dcdomain = config.shb.ldap.dcdomain;
passwordFile = pkgs.writeText "ldapUserPassword" "ldapUserPassword";
};
sso = {
enable = true;
endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}";
secretFile = pkgs.writeText "ssoSecretFile" "ssoSecretFile";
};
};
# Nginx port.
networking.firewall.allowedTCPPorts = [ 80 443 ];
};
nodes.client = {};
# TODO: Test login with ldap user
testScript = { nodes, ... }: ''
import json
import os
import pathlib
def curl(target, format, endpoint):
return json.loads(target.succeed(
"curl --fail-with-body --silent --show-error --output /dev/null --location"
+ " --connect-to j.example.com:443:server:443"
+ " --connect-to j.example.com:80:server:80"
+ f" --write-out '{format}'"
+ " " + endpoint
))
start_all()
server.wait_for_unit("jellyfin.service")
server.wait_for_unit("nginx.service")
server.wait_for_unit("lldap.service")
server.wait_for_unit("authelia-auth.example.com.service")
server.wait_for_open_port(8096)
server.copy_from_vm("/etc/ssl/certs/ca-certificates.crt")
client.succeed("rm -r /etc/ssl/certs")
client.copy_from_host(str(pathlib.Path(os.environ.get("out", os.getcwd())) / "ca-certificates.crt"), "/etc/ssl/certs/ca-certificates.crt")
response = curl(client, """{"code":%{response_code}}""", "https://j.example.com")
if response['code'] != 200:
raise Exception(f"Code is {response['code']}")
'';
};
}