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 "authelia" ./test/vm/authelia.nix)
// (vm_test "jellyfin" ./test/vm/jellyfin.nix)
// (vm_test "ldap" ./test/vm/ldap.nix) // (vm_test "ldap" ./test/vm/ldap.nix)
// (vm_test "lib" ./test/vm/lib.nix) // (vm_test "lib" ./test/vm/lib.nix)
// (vm_test "postgresql" ./test/vm/postgresql.nix) // (vm_test "postgresql" ./test/vm/postgresql.nix)

View file

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

View file

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