From a5b9a62f73bdf8979b9191cffe585c2e8f8962d5 Mon Sep 17 00:00:00 2001 From: ibizaman Date: Sat, 10 Jan 2026 15:47:00 +0100 Subject: [PATCH] jellyfin: declarative plugins, ldap and sso --- docs/redirects.json | 36 ++-- modules/services/jellyfin.nix | 124 ++++++++++-- modules/services/jellyfin/docs/default.md | 191 +++++++----------- test/common.nix | 21 +- test/services/jellyfin.nix | 226 +++++++++++++++++++--- 5 files changed, 414 insertions(+), 184 deletions(-) diff --git a/docs/redirects.json b/docs/redirects.json index 73ea0dc..f863386 100644 --- a/docs/redirects.json +++ b/docs/redirects.json @@ -3512,12 +3512,21 @@ "services-jellyfin": [ "services-jellyfin.html#services-jellyfin" ], + "services-jellyfin-certs": [ + "services-jellyfin.html#services-jellyfin-certs" + ], "services-jellyfin-debug": [ "services-jellyfin.html#services-jellyfin-debug" ], + "services-jellyfin-declarative-ldap": [ + "services-jellyfin.html#services-jellyfin-declarative-ldap" + ], "services-jellyfin-features": [ "services-jellyfin.html#services-jellyfin-features" ], + "services-jellyfin-impermanence": [ + "services-jellyfin.html#services-jellyfin-impermanence" + ], "services-jellyfin-options": [ "services-jellyfin.html#services-jellyfin-options" ], @@ -3632,12 +3641,18 @@ "services-jellyfin-options-shb.jellyfin.ldap.host": [ "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.ldap.host" ], + "services-jellyfin-options-shb.jellyfin.ldap.plugin": [ + "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.ldap.plugin" + ], "services-jellyfin-options-shb.jellyfin.ldap.port": [ "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.ldap.port" ], "services-jellyfin-options-shb.jellyfin.ldap.userGroup": [ "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.ldap.userGroup" ], + "services-jellyfin-options-shb.jellyfin.plugins": [ + "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.plugins" + ], "services-jellyfin-options-shb.jellyfin.port": [ "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.port" ], @@ -3659,9 +3674,6 @@ "services-jellyfin-options-shb.jellyfin.sso": [ "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.sso" ], - "services-jellyfin-options-shb.jellyfin.sso.adminUserGroup": [ - "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.sso.adminUserGroup" - ], "services-jellyfin-options-shb.jellyfin.sso.authorization_policy": [ "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.sso.authorization_policy" ], @@ -3674,6 +3686,9 @@ "services-jellyfin-options-shb.jellyfin.sso.endpoint": [ "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.sso.endpoint" ], + "services-jellyfin-options-shb.jellyfin.sso.plugin": [ + "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.sso.plugin" + ], "services-jellyfin-options-shb.jellyfin.sso.provider": [ "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.sso.provider" ], @@ -3725,9 +3740,6 @@ "services-jellyfin-options-shb.jellyfin.sso.sharedSecretForAuthelia.result.path": [ "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.sso.sharedSecretForAuthelia.result.path" ], - "services-jellyfin-options-shb.jellyfin.sso.userGroup": [ - "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.sso.userGroup" - ], "services-jellyfin-options-shb.jellyfin.subdomain": [ "services-jellyfin.html#services-jellyfin-options-shb.jellyfin.subdomain" ], @@ -3737,18 +3749,6 @@ "services-jellyfin-usage-backup": [ "services-jellyfin.html#services-jellyfin-usage-backup" ], - "services-jellyfin-usage-configuration": [ - "services-jellyfin.html#services-jellyfin-usage-configuration" - ], - "services-jellyfin-usage-https": [ - "services-jellyfin.html#services-jellyfin-usage-https" - ], - "services-jellyfin-usage-ldap": [ - "services-jellyfin.html#services-jellyfin-usage-ldap" - ], - "services-jellyfin-usage-sso": [ - "services-jellyfin.html#services-jellyfin-usage-sso" - ], "services-karakeep": [ "services-karakeep.html#services-karakeep" ], diff --git a/modules/services/jellyfin.nix b/modules/services/jellyfin.nix index 83114b2..28417e0 100644 --- a/modules/services/jellyfin.nix +++ b/modules/services/jellyfin.nix @@ -57,6 +57,34 @@ let platforms = dotnet-runtime.meta.platforms; }; }; + + pluginName = src: (builtins.fromJSON (builtins.readFile "${src}/meta.json")).name; + + mkJellyfinPlugin = + { + pname, + version, + hash, + url, + }: + pkgs.callPackage ( + { stdenv, fetchzip }: + stdenv.mkDerivation (finalAttrs: { + inherit pname version; + + src = fetchzip { + inherit url hash; + stripRoot = false; + }; + + dontBuild = true; + + installPhase = '' + mkdir $out + cp -r . $out + ''; + }) + ) { }; in { options.shb.jellyfin = { @@ -119,6 +147,21 @@ in ); }; + plugins = lib.mkOption { + description = '' + Install plugins declaratively. + + The LDAP and SSO plugins will be added if their respective + shb.jellyfin.ldap.enable and shb.jellyfin.sso.enable options are set to true. + + The interface for plugin creation is WIP. + Feel free to add yours following the examples from the LDAP and SSO plugins + but know that they may require some tweaks later on. + ''; + default = [ ]; + type = types.listOf types.package; + }; + ldap = lib.mkOption { description = "LDAP configuration."; default = { }; @@ -126,6 +169,17 @@ in options = { enable = lib.mkEnableOption "LDAP"; + plugin = lib.mkOption { + type = lib.types.package; + description = "Pluging used for LDAP authentication."; + default = mkJellyfinPlugin (rec { + pname = "jellyfin-plugin-ldapauth"; + version = "20"; + url = "https://github.com/jellyfin/${pname}/releases/download/v${version}/ldap-authentication_${version}.0.0.0.zip"; + hash = "sha256-qATHNuiC6u+/vbY610jrFZSC16FG+Zpdjo+dfOVdVIk="; + }); + }; + host = lib.mkOption { type = types.str; description = "Host serving the LDAP server."; @@ -178,6 +232,17 @@ in options = { enable = lib.mkEnableOption "SSO"; + plugin = lib.mkOption { + type = lib.types.package; + description = "Pluging used for SSO authentication."; + default = mkJellyfinPlugin (rec { + pname = "jellyfin-plugin-sso"; + version = "3.5.2.4"; + url = "https://github.com/9p4/${pname}/releases/download/v${version}/sso-authentication_${version}.zip"; + hash = "sha256-e+w5m6/7vRAynStDj34eBexfCIEgDJ09huHzi5gQEbo="; + }); + }; + provider = lib.mkOption { type = types.str; description = "OIDC provider name"; @@ -196,18 +261,6 @@ in default = "jellyfin"; }; - adminUserGroup = lib.mkOption { - type = types.str; - description = "OIDC admin group"; - default = "jellyfin_admin"; - }; - - userGroup = lib.mkOption { - type = types.str; - description = "OIDC user group"; - default = "jellyfin_user"; - }; - authorization_policy = lib.mkOption { type = types.enum [ "one_factor" @@ -270,6 +323,16 @@ in [ "shb" "jellyfin" "adminPassword" ] [ "shb" "jellyfin" "admin" "password" ] ) + + # (lib.mkRenamedOptionModule + # [ "shb" "jellyfin" "sso" "userGroup" ] + # [ "shb" "jellyfin" "ldap" "userGroup" ] + # ) + + # (lib.mkRenamedOptionModule + # [ "shb" "jellyfin" "sso" "adminUserGroup" ] + # [ "shb" "jellyfin" "ldap" "adminGroup" ] + # ) ]; config = lib.mkIf cfg.enable { @@ -471,10 +534,10 @@ in true - ${cfg.sso.adminUserGroup} + ${cfg.ldap.adminGroup} - ${cfg.sso.userGroup} + ${cfg.ldap.userGroup} false @@ -596,7 +659,7 @@ in ]; }) + lib.strings.optionalString cfg.ldap.enable ( - shb.replaceSecretsScript { + (shb.replaceSecretsScript { file = ldapConfig; resultPath = "${config.services.jellyfin.dataDir}/plugins/configurations/LDAP-Auth.xml"; replacements = [ @@ -605,7 +668,7 @@ in source = cfg.ldap.adminPassword.result.path; } ]; - } + }) ) + lib.strings.optionalString cfg.sso.enable ( shb.replaceSecretsScript { @@ -626,8 +689,35 @@ in replacements = [ ]; } + ) + + ( + let + pluginInstallScript = p: '' + pluginDir="${config.services.jellyfin.dataDir}/plugins/${pluginName p}" + mkdir -p "$pluginDir" + for f in "${p}"/*; do + ln -sf "$f" "$pluginDir" + done + + rm "$pluginDir/meta.json" + ${pkgs.jq}/bin/jq ". + { + status: \"Active\", + autoUpdate: false, + assemblies: [] + }" "${p}/meta.json" > "$pluginDir/meta.json" + ''; + in + lib.concatMapStringsSep "\n" pluginInstallScript cfg.plugins ); + shb.jellyfin.plugins = + lib.optionals cfg.ldap.enable [ cfg.ldap.plugin ] + ++ lib.optionals cfg.sso.enable [ cfg.sso.plugin ]; + + systemd.tmpfiles.rules = lib.optionals cfg.ldap.enable [ + "d '${config.services.jellyfin.dataDir}/plugins' 0750 jellyfin jellyfin - -" + ]; + systemd.services.jellyfin.serviceConfig.ExecStartPost = let # We must always wait for the service to be fully initialized, @@ -718,7 +808,7 @@ in systemd.services.jellyfin.serviceConfig.TimeoutStartSec = 300; - shb.authelia.oidcClients = lib.lists.optionals (!(isNull cfg.sso)) [ + shb.authelia.oidcClients = lib.optionals cfg.sso.enable [ { client_id = cfg.sso.clientID; client_name = "Jellyfin"; diff --git a/modules/services/jellyfin/docs/default.md b/modules/services/jellyfin/docs/default.md index 9a13983..53b6e56 100644 --- a/modules/services/jellyfin/docs/default.md +++ b/modules/services/jellyfin/docs/default.md @@ -13,17 +13,21 @@ and LDAP and SSO integration. - Declarative creation of admin user. - Declarative selection of listening port. -- Access through [subdomain](#services-jellyfin-options-shb.jellyfin.subdomain) using reverse proxy. [Manual](#services-jellyfin-usage-configuration). -- Access through [HTTPS](#services-jellyfin-options-shb.jellyfin.ssl) using reverse proxy. [Manual](#services-jellyfin-usage-https). -- Declarative [LDAP](#services-jellyfin-options-shb.jellyfin.ldap) configuration although plugin must be installed manually. [Manual](#services-jellyfin-usage-ldap). -- Declarative [SSO](#services-jellyfin-options-shb.jellyfin.sso) configuration although plugin must be installed manually. [Manual](#services-jellyfin-usage-sso). +- Access through [subdomain](#services-jellyfin-options-shb.jellyfin.subdomain) + and [HTTPS](#services-jellyfin-options-shb.jellyfin.ssl) using reverse proxy. [Manual](#services-jellyfin-usage). +- Declarative plugin installation. [Manual](#services-jellyfin-options-shb.jellyfin.plugins). +- Declarative [LDAP](#services-jellyfin-options-shb.jellyfin.ldap) configuration. +- Declarative [SSO](#services-jellyfin-options-shb.jellyfin.sso) configuration. - [Backup](#services-jellyfin-options-shb.jellyfin.backup) through the [backup block](./blocks-backup.html). [Manual](#services-jellyfin-usage-backup). ## Usage {#services-jellyfin-usage} -### Initial Configuration {#services-jellyfin-usage-configuration} +The following snippet assumes a few blocks have been setup already: -The following snippet enables Jellyfin and makes it available under the `jellyfin.example.com` endpoint. +- the [secrets block](usage.html#usage-secrets) with SOPS, +- the [`shb.ssl` block](blocks-ssl.html#usage), +- the [`shb.lldap` block](blocks-lldap.html#blocks-lldap-global-setup). +- the [`shb.authelia` block](blocks-authelia.html#blocks-sso-global-setup). ```nix shb.jellyfin = { @@ -33,137 +37,59 @@ shb.jellyfin = { admin = { username = "admin"; - password.result = config.shb.sops.secret.jellyfinAdminPassword.result; + password.result = config.shb.sops.secret."jellyfin/adminPassword".result; + }; + + ldap = { + enable = true; + host = "127.0.0.1"; + port = config.shb.lldap.ldapPort; + dcdomain = config.shb.lldap.dcdomain; + adminPassword.result = config.shb.sops.secrets."jellyfin/ldap/adminPassword".result + }; + + sso = { + enable = true; + endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; + + secretFile = config.shb.sops.secret."jellyfin/sso_secret".result; + secretFileForAuthelia = config.shb.sops.secret."jellyfin/authelia/sso_secret".result; }; }; -shb.sops.secret.jellyfinAdminPassword.request = config.shb.jellyfin.admin.password.request; -``` +shb.sops.secret."jellyfin/adminPassword".request = config.shb.jellyfin.admin.password.request; -This assumes secrets are setup with SOPS -as mentioned in [the secrets setup section](usage.html#usage-secrets) of the manual. +shb.sops.secrets."jellyfin/ldap/adminPassword".request = config.shb.jellyfin.ldap.adminPassword.request; -### Jellyfin through HTTPS {#services-jellyfin-usage-https} - -:::: {.note} -We will build upon the [Initial Configuration](#services-jellyfin-usage-configuration) section, -so please follow that first. -:::: - -If the `shb.ssl` block is used (see [manual](blocks-ssl.html#usage) on how to set it up), -the instance will be reachable at `https://jellyfin.example.com`. - -Here is an example with Let's Encrypt certificates, validated using the HTTP method. -First, set the global configuration for your domain: - -```nix -shb.certs.certs.letsencrypt."example.com" = { - domain = "example.com"; - group = "nginx"; - reloadServices = [ "nginx.service" ]; - adminEmail = "myemail@mydomain.com"; +shb.sops.secret."jellyfin/sso_secret".request = config.shb.jellyfin.sso.sharedSecret.request; +shb.sops.secret."jellyfin/authelia/sso_secret" = { + request = config.shb.jellyfin.sso.sharedSecretForAuthelia.request; + settings.key = "jellyfin/sso_secret"; }; ``` -Then you can tell Jellyfin to use those certificates. +Secrets can be randomly generated with `nix run nixpkgs#openssl -- rand -hex 64`. -```nix -shb.certs.certs.letsencrypt."example.com".extraDomains = [ "jellyfin.example.com" ]; - -shb.jellyfin = { - ssl = config.shb.certs.certs.letsencrypt."example.com"; -}; -``` - -### With LDAP Support {#services-jellyfin-usage-ldap} - -:::: {.note} -We will build upon the [HTTPS](#services-jellyfin-usage-https) section, -so please follow that first. -:::: - -We will use the [LLDAP block][] provided by Self Host Blocks. -Assuming it [has been set already][LLDAP block setup], add the following configuration: - -[LLDAP block]: blocks-lldap.html -[LLDAP block setup]: blocks-lldap.html#blocks-lldap-global-setup - -```nix -shb.jellyfin.ldap - enable = true; - host = "127.0.0.1"; - port = config.shb.lldap.ldapPort; - dcdomain = config.shb.lldap.dcdomain; - adminPassword.result = config.shb.sops.secrets."jellyfin/ldap/adminPassword".result -}; - -shb.sops.secrets."jellyfin/ldap/adminPassword" = { - request = config.shb.jellyfin.ldap.adminPassword.request; - settings.key = "ldap/userPassword"; -}; -``` - -The `shb.jellyfin.ldap.adminPasswordFile` must be the same -as the `shb.lldap.ldapUserPasswordFile` which is achieved -with the `key` option. -The other secrets can be randomly generated with -`nix run nixpkgs#openssl -- rand -hex 64`. - -Then, install the plugin [LDAP-Auth](https://github.com/jellyfin/jellyfin-plugin-ldapauth). -It should be available from the official repository already. -No manual configuration of the plugin is needed, just installation. -Note that the version tested with is 19.0.0.0. -If your version differs, it could lead to some configuration mismatch. -If that's the case, please [open an issue](https://github.com/ibizaman/selfhostblocks/issues/new) -or join the [support channel](https://img.shields.io/matrix/selfhostblocks%3Amatrix.org). - -And that's it. -Now, go to the LDAP server at `http://ldap.example.com`, -create the `jellyfin_user` and `jellyfin_admin` groups, -create a user and add it to one or both groups. -When that's done, go back to the Jellyfin server at -`http://jellyfin.example.com` and login with that user. - -Work is in progress to make the creation of the LDAP user and group declarative too. - -### With SSO Support {#services-jellyfin-usage-sso} - -:::: {.note} -We will build upon the [LDAP](#services-jellyfin-usage-ldap) section, -so please follow that first. -:::: - -We will use the [SSO block][] provided by Self Host Blocks. -Assuming it [has been set already][SSO block setup], add the following configuration: - -[SSO block]: blocks-sso.html -[SSO block setup]: blocks-sso.html#blocks-sso-global-setup - -```nix -shb.jellyfin.sso = { - enable = true; - endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; - - secretFile = ; - secretFileForAuthelia = ; -}; -``` - -Passing the `ssl` option will auto-configure nginx to force SSL connections with the given -certificate. +The [user](#services-jellyfin-options-shb.jellyfin.ldap.userGroup) +and [admin](#services-jellyfin-options-shb.jellyfin.ldap.adminGroup) +LDAP groups are created automatically. The `shb.jellyfin.sso.secretFile` and `shb.jellyfin.sso.secretFileForAuthelia` options must have the same content. The former is a file that must be owned by the `jellyfin` user while the latter must be owned by the `authelia` user. I want to avoid needing to define the same secret twice with a future secrets SHB block. -Then, install the plugin [SSO-Auth](https://github.com/9p4/jellyfin-plugin-sso). -It should be available from the official repository already. -No manual configuration of the plugin is needed, just installation. -Note that the version tested with is 3.5.2.4. -If your version differs, it could lead to some configuration mismatch. -If that's the case, please [open an issue](https://github.com/ibizaman/selfhostblocks/issues/new) -or join the [support channel](https://img.shields.io/matrix/selfhostblocks%3Amatrix.org). +### Certificates {#services-jellyfin-certs} + +For Let's Encrypt certificates, add: + +```nix +{ + shb.certs.certs.letsencrypt.${domain}.extraDomains = [ + "${config.shb.jellyfin.subdomain}.${config.shb.jellyfin.domain}" + ]; +} +``` ### Backup {#services-jellyfin-usage-backup} @@ -185,6 +111,27 @@ You can define any number of Restic instances to backup Jellyfin multiple times. You will then need to configure more options like the `repository`, as explained in the [restic](blocks-restic.html) documentation. +### Impermanence {#services-jellyfin-impermanence} + +To save the data folder in an impermanence setup, add: + +```nix +{ + shb.zfs.datasets."safe/jellyfin".path = config.shb.jellyfin.impermanence; +} +``` + +### Declarative LDAP {#services-jellyfin-declarative-ldap} + +To add a user `USERNAME` to the user and admin groups for jellyfin, add: + +```nix +shb.lldap.ensureUsers.USERNAME.groups = [ + config.shb.jellyfin.ldap.userGroup + config.shb.jellyfin.ldap.adminGroup +]; +``` + ## Debug {#services-jellyfin-debug} In case of an issue, check the logs for systemd service `jellyfin.service`. diff --git a/test/common.nix b/test/common.nix index e908d9a..c3216b2 100644 --- a/test/common.nix +++ b/test/common.nix @@ -283,6 +283,10 @@ in type = str; default = "[Ll]ogin"; }; + loginSpawnsNewPage = mkOption { + type = bool; + default = false; + }; testLoginWith = mkOption { type = listOf (submodule { options = { @@ -350,6 +354,8 @@ in with open("${testCfg}") as f: testCfg = json.load(f) + print("Test configuration:") + print(json.dumps(testCfg, indent=2)) browser_name = testCfg['browser'] browser_args = browsers.get(browser_name) @@ -366,11 +372,24 @@ in context.tracing.start(screenshots=True, snapshots=True, sources=True) try: page = context.new_page() + # This is used to debug frame changes. + # Frame changes or popup are somewhat handled with the expect_page() call later. + page.on("framenavigated", lambda frame: print("NAV:", frame.url)) + page.on("frameattached", lambda frame: print("ATTACHED:", frame.url)) + page.on("framedetached", lambda frame: print("DETACHED:", frame.url)) + print(f"Going to {testCfg['startUrl']}") page.goto(testCfg['startUrl']) if testCfg.get("beforeHook") is not None: - exec(testCfg.get("beforeHook")) + if testCfg['loginSpawnsNewPage']: + print("Login spawns new page") + # The with clause handles window.open() or . + with context.expect_page() as p: + exec(testCfg.get("beforeHook")) + page = p.value + else: + exec(testCfg.get("beforeHook")) if u['username'] is not None: print(f"Filling field username with {u['username']}") diff --git a/test/services/jellyfin.nix b/test/services/jellyfin.nix index c090d97..394401d 100644 --- a/test/services/jellyfin.nix +++ b/test/services/jellyfin.nix @@ -79,6 +79,7 @@ let { config, ... }: { imports = [ + shb.test.baseModule shb.test.clientLoginModule ]; virtualisation.memorySize = 4096; @@ -89,28 +90,28 @@ let test.login = { browser = "firefox"; - # I tried without the path part but it randomly selects either the wizard - # or the page that selects a server. - # startUrl = "${config.test.proto}://${config.test.fqdn}/web/#/wizardstart.html"; - # startUrl = "${config.test.proto}://${config.test.fqdn}"; - startUrl = "${config.test.proto}://${config.test.fqdn}/web/#/login.html"; + startUrl = "${config.test.proto}://${config.test.fqdn}"; usernameFieldLabelRegex = "[Uu]ser"; loginButtonNameRegex = "Sign In"; testLoginWith = [ - # I just couldn't make this work. It's very flaky. - # Most of the time, the login jellyfin page doesn't even load - # and the playwright browser is stuck on the splash page. - # I resorted to test the API directly. - # { username = "jellyfin"; password = "badpassword"; nextPageExpect = [ - # "expect(page).to_have_title(re.compile('Jellyfin'))" - # "expect(page.get_by_text(re.compile('[Ii]nvalid'))).to_be_visible(timeout=30000)" - # ]; } - # { username = "jellyfin"; password = "admin"; nextPageExpect = [ - # "expect(page).to_have_title(re.compile('Jellyfin'))" - # "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=30000)" - # "expect(page.get_by_role('label', re.compile('[Uu]ser'))).not_to_be_visible(timeout=30000)" - # "expect(page.get_by_text(re.compile('[Pp]assword'))).not_to_be_visible(timeout=30000)" - # ]; } + { + username = "jellyfin"; + password = "badpassword"; + nextPageExpect = [ + "expect(page).to_have_title(re.compile('Jellyfin'))" + "expect(page.get_by_text(re.compile('[Ii]nvalid'))).to_be_visible(timeout=10000)" + ]; + } + { + username = "jellyfin"; + password = "admin"; + nextPageExpect = [ + "expect(page).to_have_title(re.compile('Jellyfin'))" + "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=10000)" + "expect(page.get_by_label(re.compile('^[Uu]ser'))).not_to_be_visible(timeout=10000)" + "expect(page.get_by_label(re.compile('^[Pp]assword$'))).not_to_be_visible(timeout=10000)" + ]; + } ]; }; }; @@ -135,6 +136,8 @@ let host = "127.0.0.1"; port = config.shb.lldap.ldapPort; dcdomain = config.shb.lldap.dcdomain; + userGroup = "user_group"; + adminGroup = "admin_group"; adminPassword.result = config.shb.hardcodedsecret.jellyfinLdapUserPassword.result; }; }; @@ -145,10 +148,95 @@ let }; }; + clientLoginLdap = + { config, ... }: + { + imports = [ + shb.test.baseModule + shb.test.clientLoginModule + ]; + virtualisation.memorySize = 4096; + + test = { + subdomain = "j"; + }; + + test.login = { + startUrl = "${config.test.proto}://${config.test.fqdn}"; + usernameFieldLabelRegex = "[Uu]ser"; + loginButtonNameRegex = "Sign In"; + testLoginWith = [ + { + username = "jellyfin"; + password = "badpassword"; + nextPageExpect = [ + "expect(page).to_have_title(re.compile('Jellyfin'))" + "expect(page.get_by_text(re.compile('[Ii]nvalid'))).to_be_visible(timeout=10000)" + ]; + } + { + username = "jellyfin"; + password = "admin"; + nextPageExpect = [ + "expect(page).to_have_title(re.compile('Jellyfin'))" + "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=10000)" + "expect(page.get_by_label(re.compile('^[Uu]ser'))).not_to_be_visible(timeout=10000)" + "expect(page.get_by_label(re.compile('^[Pp]assword$'))).not_to_be_visible(timeout=10000)" + ]; + } + { + username = "alice"; + password = "AlicePassword"; + nextPageExpect = [ + "expect(page).to_have_title(re.compile('Jellyfin'))" + # For a reason I can't explain, redirection needs to happen manually. + "page.goto('${config.test.proto}://${config.test.fqdn}/web/')" + "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=10000)" + "expect(page.get_by_label(re.compile('^[Uu]ser'))).not_to_be_visible(timeout=10000)" + "expect(page.get_by_label(re.compile('^[Pp]assword$'))).not_to_be_visible(timeout=10000)" + ]; + } + { + username = "alice"; + password = "NotAlicePassword"; + nextPageExpect = [ + "expect(page).to_have_title(re.compile('Jellyfin'))" + "expect(page.get_by_text(re.compile('[Ii]nvalid'))).to_be_visible(timeout=10000)" + ]; + } + { + username = "bob"; + password = "BobPassword"; + nextPageExpect = [ + "expect(page).to_have_title(re.compile('Jellyfin'))" + # For a reason I can't explain, redirection needs to happen manually. + "page.goto('${config.test.proto}://${config.test.fqdn}/web/')" + "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=10000)" + "expect(page.get_by_label(re.compile('^[Uu]ser'))).not_to_be_visible(timeout=10000)" + "expect(page.get_by_label(re.compile('^[Pp]assword$'))).not_to_be_visible(timeout=10000)" + ]; + } + { + username = "bob"; + password = "NotBobPassword"; + nextPageExpect = [ + "expect(page).to_have_title(re.compile('Jellyfin'))" + "expect(page.get_by_text(re.compile('[Ii]nvalid'))).to_be_visible(timeout=10000)" + ]; + } + ]; + }; + }; + sso = { config, ... }: { shb.jellyfin = { + ldap = { + userGroup = "user_group"; + adminGroup = "admin_group"; + }; + sso = { enable = true; endpoint = "https://${config.shb.authelia.subdomain}.${config.shb.authelia.domain}"; @@ -168,6 +256,82 @@ let }; }; + clientLoginSso = + { config, ... }: + { + imports = [ + shb.test.baseModule + shb.test.clientLoginModule + ]; + virtualisation.memorySize = 4096; + + test = { + subdomain = "j"; + }; + + test.login = { + startUrl = "${config.test.proto}://${config.test.fqdn}"; + beforeHook = '' + page.locator('text=Sign in with Authelia').click() + ''; + usernameFieldLabelRegex = "Username"; + passwordFieldLabelRegex = "Password"; + loginButtonNameRegex = "[Ss]ign [Ii]n"; + loginSpawnsNewPage = true; + testLoginWith = [ + { + username = "alice"; + password = "AlicePassword"; + nextPageExpect = [ + "page.get_by_text(re.compile('[Aa]ccept')).click()" + # For a reason I can't explain, redirection needs to happen manually. + "page.goto('${config.test.proto}://${config.test.fqdn}/web/')" + "expect(page).to_have_title(re.compile('Jellyfin'))" + "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=10000)" + "expect(page.get_by_label(re.compile('^[Uu]ser'))).not_to_be_visible(timeout=10000)" + "expect(page.get_by_label(re.compile('^[Pp]assword$'))).not_to_be_visible(timeout=10000)" + ]; + } + { + username = "alice"; + password = "NotAlicePassword"; + nextPageExpect = [ + # For a reason I can't explain, redirection needs to happen manually. + # So for failing auth, we check we're back on the login page. + "page.goto('${config.test.proto}://${config.test.fqdn}/web/')" + "expect(page).to_have_title(re.compile('Jellyfin'))" + "expect(page.get_by_label(re.compile('^[Uu]ser'))).to_be_visible(timeout=10000)" + "expect(page.get_by_label(re.compile('^[Pp]assword$'))).to_be_visible(timeout=10000)" + ]; + } + { + username = "bob"; + password = "BobPassword"; + nextPageExpect = [ + "page.get_by_text(re.compile('[Aa]ccept')).click()" + # For a reason I can't explain, redirection needs to happen manually. + "page.goto('${config.test.proto}://${config.test.fqdn}/web/')" + "expect(page).to_have_title(re.compile('Jellyfin'))" + "expect(page.get_by_text(re.compile('[Ii]nvalid'))).not_to_be_visible(timeout=10000)" + "expect(page.get_by_label(re.compile('^[Uu]ser'))).not_to_be_visible(timeout=10000)" + "expect(page.get_by_label(re.compile('^[Pp]assword$'))).not_to_be_visible(timeout=10000)" + ]; + } + { + username = "bob"; + password = "NotBobPassword"; + nextPageExpect = [ + # For a reason I can't explain, redirection needs to happen manually. + "page.goto('${config.test.proto}://${config.test.fqdn}/web/')" + "expect(page).to_have_title(re.compile('Jellyfin'))" + "expect(page.get_by_label(re.compile('^[Uu]ser'))).to_be_visible(timeout=10000)" + "expect(page.get_by_label(re.compile('^[Pp]assword$'))).to_be_visible(timeout=10000)" + ]; + } + ]; + }; + }; + jellyfinTest = name: { nodes, testScript }: @@ -189,13 +353,14 @@ in nodes.server = { imports = [ basic - clientLogin ]; }; - # Client login does not work without SSL. - # At least, I couldn't make it work. - nodes.client = { }; + nodes.client = { + imports = [ + clientLogin + ]; + }; testScript = commonTestScript.access; }; @@ -228,7 +393,6 @@ in { config, lib, ... }: { imports = [ - shb.test.baseModule clientLogin ]; }; @@ -240,12 +404,18 @@ in nodes.server = { imports = [ basic + shb.test.certs + https shb.test.ldap ldap ]; }; - nodes.client = { }; + nodes.client = { + imports = [ + clientLoginLdap + ]; + }; testScript = commonTestScript.access; }; @@ -264,7 +434,11 @@ in ]; }; - nodes.client = { }; + nodes.client = { + imports = [ + clientLoginSso + ]; + }; testScript = commonTestScript.access; };