jellyfin: declarative plugins, ldap and sso
This commit is contained in:
parent
6d9831e4f8
commit
a5b9a62f73
5 changed files with 414 additions and 184 deletions
|
|
@ -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"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
|
|
|||
|
|
@ -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']}")
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue