jellyfin: declarative plugins, ldap and sso

This commit is contained in:
ibizaman 2026-01-10 15:47:00 +01:00
parent 6d9831e4f8
commit d9f40c7155
5 changed files with 414 additions and 184 deletions

View file

@ -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"
],

View file

@ -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
<EnableAllFolders>true</EnableAllFolders>
<EnabledFolders />
<AdminRoles>
<string>${cfg.sso.adminUserGroup}</string>
<string>${cfg.ldap.adminGroup}</string>
</AdminRoles>
<Roles>
<string>${cfg.sso.userGroup}</string>
<string>${cfg.ldap.userGroup}</string>
</Roles>
<EnableFolderRoles>false</EnableFolderRoles>
<FolderRoleMappings />
@ -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";

View file

@ -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 = <path/to/oidcJellyfinSharedSecret>;
secretFileForAuthelia = <path/to/oidcJellyfinSharedSecret>;
};
```
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`.

View file

@ -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 <a target="_blank">.
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']}")

View file

@ -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;
};