1
0
Fork 0

Merge branch 'main' into audiobookshelf

This commit is contained in:
ibizaman 2024-03-03 16:44:12 -08:00
commit 3bf84c732e
17 changed files with 1045 additions and 175 deletions

View file

@ -230,21 +230,16 @@ SOPS_AGE_KEY_FILE=keys.txt nix run --impure nixpkgs#sops -- \
The `secrets.yaml` file must follow the format:
```yaml
home-assistant: |
name: "My Instance"
home-assistant:
country: "US"
latitude_home: "0.100"
longitude_home: "-0.100"
latitude: "0.100"
longitude: "-0.100"
time_zone: "America/Los_Angeles"
unit_system: "metric"
lldap:
user_password: XXX...
jwt_secret: YYY...
```
> Important: the value of the `home-assistant` field is a string that looks like yaml. Do _not_
> remove the pipe (|) sign.
You can generate random secrets with:
```bash

View file

@ -5,11 +5,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"lastModified": 1709126324,
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
"type": "github"
},
"original": {
@ -35,11 +35,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1707092692,
"narHash": "sha256-ZbHsm+mGk/izkWtT4xwwqz38fdlwu7nUUKXTOmm4SyE=",
"lastModified": 1709150264,
"narHash": "sha256-HofykKuisObPUfj0E9CJVfaMhawXkYx3G8UIFR/XQ38=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "faf912b086576fd1a15fca610166c98d47bc667e",
"rev": "9099616b93301d5cf84274b184a3a5ec69e94e08",
"type": "github"
},
"original": {
@ -51,27 +51,27 @@
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1705957679,
"narHash": "sha256-Q8LJaVZGJ9wo33wBafvZSzapYsjOaNjP/pOnSiKVGHY=",
"lastModified": 1708819810,
"narHash": "sha256-1KosU+ZFXf31GPeCBNxobZWMgHsSOJcrSFA6F2jhzdE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9a333eaa80901efe01df07eade2c16d183761fa3",
"rev": "89a2a12e6c8c6a56c72eb3589982c8e2f89c70ea",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-23.05",
"ref": "release-23.11",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1706925685,
"narHash": "sha256-hVInjWMmgH4yZgA4ZtbgJM1qEAel72SYhP5nOWX4UIM=",
"lastModified": 1708751719,
"narHash": "sha256-0uWOKSpXJXmXswOvDM5Vk3blB74apFB6rNGWV5IjoN0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "79a13f1437e149dc7be2d1290c74d378dad60814",
"rev": "f63ce824cd2f036216eb5f637dfef31e1a03ee89",
"type": "github"
},
"original": {
@ -111,11 +111,11 @@
"sops-nix": "sops-nix"
},
"locked": {
"lastModified": 1707374005,
"narHash": "sha256-W3p8hBLUdlHAG7yxT250jImnFmXe83tN119/jRiBYdo=",
"lastModified": 1709267447,
"narHash": "sha256-5Q467FhpS18L/+5iB3wsWaR9tBqdzNt0fpdkZJNqNxc=",
"owner": "ibizaman",
"repo": "selfhostblocks",
"rev": "7d0276e9f2509bc6f175358c318374fedfc64422",
"rev": "fa206d0e1515fb0e49393e7ada6d7e5c6ec1df58",
"type": "github"
},
"original": {
@ -130,11 +130,11 @@
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1707015547,
"narHash": "sha256-YZr0OrqWPdbwBhxpBu69D32ngJZw8AMgZtJeaJn0e94=",
"lastModified": 1708987867,
"narHash": "sha256-k2lDaDWNTU5sBVHanYzjDKVDmk29RHIgdbbXu5sdzBA=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "23f61b897c00b66855074db471ba016e0cda20dd",
"rev": "a1c8de14f60924fafe13aea66b46157f0150f4cf",
"type": "github"
},
"original": {

View file

@ -18,7 +18,42 @@
enable = true;
domain = "example.com";
subdomain = "ha";
config = {
name = "SHB Home Assistant";
country.source = config.sops.secrets."home-assistant/country".path;
latitude.source = config.sops.secrets."home-assistant/latitude".path;
longitude.source = config.sops.secrets."home-assistant/longitude".path;
time_zone.source = config.sops.secrets."home-assistant/time_zone".path;
unit_system = "metric";
};
};
sops.secrets."home-assistant/country" = {
sopsFile = ./secrets.yaml;
mode = "0440";
owner = "hass";
group = "hass";
restartUnits = [ "home-assistant.service" ];
};
sops.secrets."home-assistant/latitude" = {
sopsFile = ./secrets.yaml;
mode = "0440";
owner = "hass";
group = "hass";
restartUnits = [ "home-assistant.service" ];
};
sops.secrets."home-assistant/longitude" = {
sopsFile = ./secrets.yaml;
mode = "0440";
owner = "hass";
group = "hass";
restartUnits = [ "home-assistant.service" ];
};
sops.secrets."home-assistant/time_zone" = {
sopsFile = ./secrets.yaml;
mode = "0440";
owner = "hass";
group = "hass";
restartUnits = [ "home-assistant.service" ];
};
nixpkgs.config.permittedInsecurePackages = [

View file

@ -1,4 +1,8 @@
home-assistant: ENC[AES256_GCM,data:acEXqx3bdQp0zB5FnHCBsic/kgu2L8Q6h/fsfrLmdk7SOfzEibPpPLCCv8eYmh4D5VuIAsq/PeJ3k+uqWGbTrJt7EIcxt0kYTLRuWZRG8YJH1+HCxoKcO/mx9bwbRd3LtXiVscgP9zIZLoLPK2XieFKOeg==,iv:dJ7FUkquMI4g4K2Nnv3kFFQk/va2QgwfgGoWif5f2tU=,tag:6LIBt9whdRPVsoF1RY3Pew==,type:str]
home-assistant:
country: ENC[AES256_GCM,data:2Ng=,iv:/VMB6yi3e8piAx8DzLGGhLsozxWUWX2R7NcmACFng8Q=,tag:Tx0Iy1AnLmPrnYu7XtbesA==,type:str]
latitude: ENC[AES256_GCM,data:p/O1HW4=,iv:CRgL4wcM3gMNu/OAHVoQuLcRD9J3SbkxsjvobiabQ0g=,tag:uIo5Rv7geOtVcarp4Qkqww==,type:str]
longitude: ENC[AES256_GCM,data:sVyww6F7,iv:9EZYXSkv+rhD77lqmC+c8i+wf46KPYloVoK+ok3bWYY=,tag:c+lmtcGvULtMdu9ZTDewjA==,type:str]
time_zone: ENC[AES256_GCM,data:JKXdsQZrtB1B77klxuemw1tZbg==,iv:nItJfpwp2XWmBHbohrjNMWQ8TpL2Xsv22UujZRgDscw=,tag:wrHbA1yycutUUn79F9wy6Q==,type:str]
lldap:
user_password: ENC[AES256_GCM,data:JrFraqFSqAhRVjB5fagIoB864aejt24q+qqWeu8ySC0=,iv:RS7VS+9tsSknn9SwpfyYVi41m3lN4SkZ4CSwrzH/Eso=,tag:5L7fx6/KhDtjHPruwac/sw==,type:str]
jwt_secret: ENC[AES256_GCM,data:W1T/QoxuzMD+2AL7sP5KkMcC+GvFdd4kfd70rHLnQD+jWNs9G0igkC/BxxgbIfnSASwtSnBaaiU6/pxLFOcUVh0Nyd0Zmb/KTbagpUvSl//AZnTt/WKF9Q/8sqKzsGv0QdMyZKWi4cxiEILcTbxOsgwriFGgOJ1k5N8JEif15ig=,iv:rHlRt6nWMz8rVmU0aKH6VWWVXunOfJcDvZOxgWbK1FI=,tag:qC6N61rE8CfPSXrsEqFoIQ==,type:str]
@ -26,8 +30,8 @@ sops:
VlJpS1BYd2UrZU1mZTEwU1BYODhqM2sKvQnFV8xsy1tEmYZu4izBYb7XQqTPOLTL
bRkU6n17uiyXNbiXDAbX0Png/XmVG96/+Zl38BBXPQvARX8c2tzq6w==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2024-01-23T00:46:58Z"
mac: ENC[AES256_GCM,data:kBkUCStabQ32JK/UDPATgOz3HoI/dVkNLsl6uEhHk8ODbF+ZBg6BDEaxtMFFh0bV+71klAmF0KsL/kHKiHlbNuoNWOxwbsANGeL8xtV6JCU58zTF0nfgAP/3KJYveridgylRRZS5hYl5Mg+z6Zdgw+43r3Iiizf86BZVc5OaDyY=,iv:ZXWLXQUrVIwYCCVnXI0jTf5paOWNuujG/Pw+Nf/M34A=,tag:+P/UJqBI3prcxEUO4Zqu/A==,type:str]
lastmodified: "2024-02-12T05:07:51Z"
mac: ENC[AES256_GCM,data:MOmvK0g6Wj+fND154QUhmXujsDOKMO5CRRckru+eDRPeHcJZUnI/jjolcI8y+LEdhUVf0Ln8E38GSxZT/8EW3CfCNkOUikGFdfxuQ2uzNp/1wMvNaF988lrXMBfQ7Il18AiYVK0QhGReGXJa6wBVUb2Qfrg41WC65UvQtMOByqI=,iv:Rscvq1l7YgNapC0NkabQHBzirzsPEr8ykAQqx+qGoi0=,tag:ud+K72bnUV1hnsjcewNrsw==,type:str]
pgp: []
unencrypted_suffix: _unencrypted
version: 3.8.1

30
flake.lock generated
View file

@ -5,11 +5,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1705309234,
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
"lastModified": 1709126324,
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
"type": "github"
},
"original": {
@ -35,11 +35,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1707956935,
"narHash": "sha256-ZL2TrjVsiFNKOYwYQozpbvQSwvtV/3Me7Zwhmdsfyu4=",
"lastModified": 1709237383,
"narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "a4d4fe8c5002202493e87ec8dbc91335ff55552c",
"rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
"type": "github"
},
"original": {
@ -51,11 +51,11 @@
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1707603439,
"narHash": "sha256-LodBVZ3+ehJP2azM5oj+JrhfNAAzmTJ/OwAIOn0RfZ0=",
"lastModified": 1708819810,
"narHash": "sha256-1KosU+ZFXf31GPeCBNxobZWMgHsSOJcrSFA6F2jhzdE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d8cd80616c8800feec0cab64331d7c3d5a1a6d98",
"rev": "89a2a12e6c8c6a56c72eb3589982c8e2f89c70ea",
"type": "github"
},
"original": {
@ -67,11 +67,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1707451808,
"narHash": "sha256-UwDBUNHNRsYKFJzyTMVMTF5qS4xeJlWoeyJf+6vvamU=",
"lastModified": 1708751719,
"narHash": "sha256-0uWOKSpXJXmXswOvDM5Vk3blB74apFB6rNGWV5IjoN0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "442d407992384ed9c0e6d352de75b69079904e4e",
"rev": "f63ce824cd2f036216eb5f637dfef31e1a03ee89",
"type": "github"
},
"original": {
@ -112,11 +112,11 @@
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1707842202,
"narHash": "sha256-3dTBbCzHJBinwhsisGJHW1HLBsLbj91+a5ZDXt7ttW0=",
"lastModified": 1708987867,
"narHash": "sha256-k2lDaDWNTU5sBVHanYzjDKVDmk29RHIgdbbXu5sdzBA=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "48afd3264ec52bee85231a7122612e2c5202fa74",
"rev": "a1c8de14f60924fafe13aea66b46157f0150f4cf",
"type": "github"
},
"original": {

View file

@ -89,14 +89,22 @@
mergeTests (importFiles [
./test/modules/arr.nix
./test/modules/davfs.nix
./test/modules/lib.nix
./test/modules/nginx.nix
./test/modules/postgresql.nix
]);
};
lib = nix-flake-tests.lib.check {
inherit pkgs;
tests = pkgs.callPackage ./test/modules/lib.nix {};
};
}
// (vm_test "audiobookshelf" ./test/vm/audiobookshelf.nix)
// (vm_test "authelia" ./test/vm/authelia.nix)
// (vm_test "jellyfin" ./test/vm/jellyfin.nix)
// (vm_test "ldap" ./test/vm/ldap.nix)
// (vm_test "lib" ./test/vm/lib.nix)
// (vm_test "postgresql" ./test/vm/postgresql.nix)
// (vm_test "monitoring" ./test/vm/monitoring.nix)
// (vm_test "nextcloud" ./test/vm/nextcloud.nix)

View file

@ -1,13 +1,110 @@
{ lib }:
{
template = file: newPath: replacements:
{ pkgs, lib }:
rec {
replaceSecrets = { userConfig, resultPath, generator }:
let
templatePath = newPath + ".template";
configWithTemplates = withReplacements userConfig;
nonSecretConfigFile = pkgs.writeText "${resultPath}.template" (generator configWithTemplates);
replacements = getReplacements userConfig;
in
replaceSecretsScript {
file = nonSecretConfigFile;
inherit resultPath replacements;
};
template = file: newPath: replacements: replaceSecretsScript { inherit file replacements; resultPath = newPath; };
replaceSecretsScript = { file, resultPath, replacements }:
let
templatePath = resultPath + ".template";
sedPatterns = lib.strings.concatStringsSep " " (lib.attrsets.mapAttrsToList (from: to: "-e \"s|${from}|${to}|\"") replacements);
in
''
set -euo pipefail
set -x
mkdir -p $(dirname ${templatePath})
ln -fs ${file} ${templatePath}
rm ${newPath} || :
sed ${sedPatterns} ${templatePath} > ${newPath}
rm -f ${resultPath}
${pkgs.gnused}/bin/sed ${sedPatterns} ${templatePath} > ${resultPath}
'';
secretFileType = lib.types.submodule {
options = {
source = lib.mkOption {
type = lib.types.path;
description = "File containing the value.";
};
transform = lib.mkOption {
type = lib.types.raw;
description = "An optional function to transform the secret.";
default = null;
example = lib.literalExpression ''
v: "prefix-$${v}-suffix"
'';
};
};
};
secretName = name:
"%SECRET${lib.strings.toUpper (lib.strings.concatMapStrings (s: "_" + s) name)}%";
withReplacements = attrs:
let
valueOrReplacement = name: value:
if !(builtins.isAttrs value && value ? "source")
then value
else secretName name;
in
mapAttrsRecursiveCond (v: ! v ? "source") valueOrReplacement attrs;
getReplacements = attrs:
let
addNameField = name: value:
if !(builtins.isAttrs value && value ? "source")
then value
else value // { name = name; };
secretsWithName = mapAttrsRecursiveCond (v: ! v ? "source") addNameField attrs;
allSecrets = collect (v: builtins.isAttrs v && v ? "source") secretsWithName;
t = { transform ? null, ... }: if isNull transform then x: x else transform;
genReplacement = secret:
lib.attrsets.nameValuePair (secretName secret.name) ((t secret) "$(cat ${toString secret.source})");
in
lib.attrsets.listToAttrs (map genReplacement allSecrets);
# Inspired lib.attrsets.mapAttrsRecursiveCond but also recurses on lists.
mapAttrsRecursiveCond =
# A function, given the attribute set the recursion is currently at, determine if to recurse deeper into that attribute set.
cond:
# A function, given a list of attribute names and a value, returns a new value.
f:
# Attribute set or list to recursively map over.
set:
let
recurse = path: val:
if builtins.isAttrs val && cond val
then lib.attrsets.mapAttrs (n: v: recurse (path ++ [n]) v) val
else if builtins.isList val && cond val
then lib.lists.imap0 (i: v: recurse (path ++ [(builtins.toString i)]) v) val
else f path val;
in recurse [] set;
# Like lib.attrsets.collect but also recurses on lists.
collect =
# Given an attribute's value, determine if recursion should stop.
pred:
# The attribute set to recursively collect.
attrs:
if pred attrs then
[ attrs ]
else if builtins.isAttrs attrs then
lib.lists.concatMap (collect pred) (lib.attrsets.attrValues attrs)
else if builtins.isList attrs then
lib.lists.concatMap (collect pred) attrs
else
[];
}

View file

@ -94,9 +94,54 @@ in
};
oidcClients = lib.mkOption {
type = lib.types.listOf lib.types.anything;
description = "OIDC clients";
default = [];
type = lib.types.listOf (lib.types.submodule {
freeformType = lib.types.attrsOf lib.types.anything;
options = {
id = lib.mkOption {
type = lib.types.str;
description = "Unique identifier of the OIDC client.";
};
description = lib.mkOption {
type = lib.types.nullOr lib.types.str;
description = "Human readable description of the OIDC client.";
default = null;
};
secret = lib.mkOption {
type = shblib.secretFileType;
description = "File containing the shared secret with the OIDC client.";
};
public = lib.mkOption {
type = lib.types.bool;
description = "If the OIDC client is public or not.";
default = false;
apply = v: if v then "true" else "false";
};
authorization_policy = lib.mkOption {
type = lib.types.enum [ "one_factor" "two_factor" ];
description = "Require one factor (password) or two factor (device) authentication.";
default = "one_factor";
};
redirect_uris = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "List of uris that are allowed to be redirected to.";
};
scopes = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "Scopes to ask for";
example = [ "openid" "profile" "email" "groups" ];
default = [];
};
};
});
};
smtp = lib.mkOption {
@ -291,13 +336,13 @@ in
systemd.services."authelia-${fqdn}".preStart =
let
mkCfg = clients:
let
addTemplate = client: (builtins.removeAttrs client ["secretFile"]) // {secret = "%SECRET_${client.id}%";};
tmplFile = pkgs.writeText "oidc_clients.yaml" (lib.generators.toYAML {} {identity_providers.oidc.clients = map addTemplate clients;});
replace = client: {"%SECRET_${client.id}%" = "$(cat ${toString client.secretFile})";};
replacements = lib.foldl (container: client: container // (replace client) ) {} clients;
in
shblib.template tmplFile "/var/lib/authelia-${fqdn}/oidc_clients.yaml" replacements;
shblib.replaceSecrets {
userConfig = {
identity_providers.oidc.clients = clients;
};
resultPath = "/var/lib/authelia-${fqdn}/oidc_clients.yaml";
generator = lib.generators.toYAML {};
};
in
lib.mkBefore (mkCfg cfg.oidcClients);

View file

@ -4,6 +4,7 @@ let
cfg = config.shb.home-assistant;
contracts = pkgs.callPackage ../contracts {};
shblib = pkgs.callPackage ../../lib {};
fqdn = "${cfg.subdomain}.${cfg.domain}";
@ -18,6 +19,15 @@ let
export PATH=${pkgs.gnused}/bin:${pkgs.curl}/bin:${pkgs.jq}/bin
exec ${pkgs.bash}/bin/bash ${ldap_auth_script_repo}/example_configs/lldap-ha-auth.sh $@
'';
# Filter secrets from config. Secrets are those of the form { source = <path>; }
secrets = lib.attrsets.filterAttrs (k: v: builtins.isAttrs v) cfg.config;
nonSecrets = (lib.attrsets.filterAttrs (k: v: !(builtins.isAttrs v)) cfg.config);
configWithSecretsIncludes =
nonSecrets
// (lib.attrsets.mapAttrs (k: v: "!secret ${k}") secrets);
in
{
options.shb.home-assistant = {
@ -41,6 +51,41 @@ in
default = null;
};
config = lib.mkOption {
description = "See all available settings at https://www.home-assistant.io/docs/configuration/basic/";
type = lib.types.submodule {
freeformType = lib.types.attrsOf lib.types.str;
options = {
name = lib.mkOption {
type = lib.types.oneOf [ lib.types.str shblib.secretFileType ];
description = "Name of the Home Assistant instance.";
};
country = lib.mkOption {
type = lib.types.oneOf [ lib.types.str shblib.secretFileType ];
description = "Two letter country code where this instance is located.";
};
latitude = lib.mkOption {
type = lib.types.oneOf [ lib.types.str shblib.secretFileType ];
description = "Latitude where this instance is located.";
};
longitude = lib.mkOption {
type = lib.types.oneOf [ lib.types.str shblib.secretFileType ];
description = "Longitude where this instance is located.";
};
time_zone = lib.mkOption {
type = lib.types.oneOf [ lib.types.str shblib.secretFileType ];
description = "Timezone of this instance.";
example = "America/Los_Angeles";
};
unit_system = lib.mkOption {
type = lib.types.oneOf [ lib.types.str (lib.types.enum [ "metric" "us_customary" ]) ];
description = "Timezone of this instance.";
example = "America/Los_Angeles";
};
};
};
};
ldap = lib.mkOption {
description = ''
LDAP Integration App. [Manual](https://docs.nextcloud.com/server/latest/admin_manual/configuration_user/user_auth_ldap.html)
@ -91,12 +136,6 @@ in
};
};
sopsFile = lib.mkOption {
type = lib.types.path;
description = "Sops file location";
example = "secrets/homeassistant.yaml";
};
backupCfg = lib.mkOption {
type = lib.types.anything;
description = "Backup configuration for home-assistant";
@ -144,14 +183,8 @@ in
trusted_proxies = "127.0.0.1";
};
logger.default = "info";
homeassistant = {
homeassistant = configWithSecretsIncludes // {
external_url = "https://${cfg.subdomain}.${cfg.domain}";
name = "!secret name";
country = "!secret country";
latitude = "!secret latitude_home";
longitude = "!secret longitude_home";
time_zone = "!secret time_zone";
unit_system = "metric";
auth_providers =
(lib.optionals (!cfg.ldap.enable || cfg.ldap.keepDefaultAuth) [
{
@ -256,23 +289,18 @@ in
}
}
'';
storage = "${config.services.home-assistant.configDir}/.storage";
file = "${storage}/onboarding";
storage = "${config.services.home-assistant.configDir}";
file = "${storage}/.storage/onboarding";
in
''
if ! -f ${file}; then
mkdir -p ${storage} && cp ${onboarding} ${file}
fi
'');
sops.secrets."home-assistant" = {
inherit (cfg) sopsFile;
mode = "0440";
owner = "hass";
group = "hass";
path = "${config.services.home-assistant.configDir}/secrets.yaml";
restartUnits = [ "home-assistant.service" ];
};
'' + shblib.replaceSecrets {
userConfig = cfg.config;
resultPath = "${config.services.home-assistant.configDir}/secrets.yaml";
generator = lib.generators.toYAML {};
});
systemd.tmpfiles.rules = [
"f ${config.services.home-assistant.configDir}/automations.yaml 0755 hass hass"

View file

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

View file

@ -82,6 +82,13 @@ in
default = "/var/lib/nextcloud";
};
mountPointServices = lib.mkOption {
description = "If given, all the systemd services and timers will depend on the specified mount point systemd services.";
type = lib.types.listOf lib.types.str;
default = [];
example = lib.literalExpression ''["var.mount"]'';
};
adminUser = lib.mkOption {
type = lib.types.str;
description = "Username of the initial admin user.";
@ -239,6 +246,27 @@ in
options = {
enable = lib.mkEnableOption "Nextcloud Preview Generator App";
recommendedSettings = lib.mkOption {
type = lib.types.bool;
description = ''
Better defaults than the defaults. Taken from [this article](http://web.archive.org/web/20200513043150/https://ownyourbits.com/2019/06/29/understanding-and-improving-nextcloud-previews/).
Sets the following options:
```
nextcloud-occ config:app:set previewgenerator squareSizes --value="32 256"
nextcloud-occ config:app:set previewgenerator widthSizes --value="256 384"
nextcloud-occ config:app:set previewgenerator heightSizes --value="256"
nextcloud-occ config:system:set preview_max_x --value 2048
nextcloud-occ config:system:set preview_max_y --value 2048
nextcloud-occ config:system:set jpeg_quality --value 60
nextcloud-occ config:app:set preview jpeg_quality --value="60"
```
'';
default = true;
example = false;
};
debug = lib.mkOption {
type = lib.types.bool;
description = "Enable more verbose logging.";
@ -595,10 +623,17 @@ in
systemd.services.phpfpm-nextcloud.preStart = ''
mkdir -p /var/log/xdebug; chown -R nextcloud: /var/log/xdebug
'';
systemd.services.phpfpm-nextcloud.requires = cfg.mountPointServices;
systemd.services.phpfpm-nextcloud.after = cfg.mountPointServices;
systemd.services.nextcloud-cron.path = [
pkgs.perl
];
systemd.timers.nextcloud-cron.requires = cfg.mountPointServices;
systemd.timers.nextcloud-cron.after = cfg.mountPointServices;
systemd.services.nextcloud-setup.requires = cfg.mountPointServices;
systemd.services.nextcloud-setup.after = cfg.mountPointServices;
# Sets up backup for Nextcloud.
shb.backup.instances.nextcloud = {
@ -649,10 +684,23 @@ in
inherit ((nextcloudApps cfg.version)) previewgenerator;
};
# Values taken from
# http://web.archive.org/web/20200513043150/https://ownyourbits.com/2019/06/29/understanding-and-improving-nextcloud-previews/
systemd.services.nextcloud-setup.script = lib.mkIf cfg.apps.previewgenerator.recommendedSettings ''
${occ} config:app:set previewgenerator squareSizes --value="32 256"
${occ} config:app:set previewgenerator widthSizes --value="256 384"
${occ} config:app:set previewgenerator heightSizes --value="256"
${occ} config:system:set preview_max_x --value 2048
${occ} config:system:set preview_max_y --value 2048
${occ} config:system:set jpeg_quality --value 60
${occ} config:app:set preview jpeg_quality --value="60"
'';
# Configured as defined in https://github.com/nextcloud/previewgenerator
systemd.timers.nextcloud-cron-previewgenerator = {
wantedBy = [ "timers.target" ];
after = [ "nextcloud-setup.service" ];
requires = cfg.mountPointServices;
after = [ "nextcloud-setup.service" ] + cfg.mountPointServices;
timerConfig.OnBootSec = "10m";
timerConfig.OnUnitActiveSec = "10m";
timerConfig.Unit = "nextcloud-cron-previewgenerator.service";
@ -829,8 +877,8 @@ in
{
id = cfg.apps.sso.clientID;
description = "Nextcloud";
secretFile = cfg.apps.sso.secretFileForAuthelia;
public = "false";
secret.source = cfg.apps.sso.secretFileForAuthelia;
public = false;
authorization_policy = cfg.apps.sso.authorization_policy;
redirect_uris = [ "${protocol}://${fqdnWithPort}/apps/oidc_login/oidc" ];
scopes = [

View file

@ -83,6 +83,18 @@ shb.nextcloud = {
After deploying, the Nextcloud server will be reachable at `http://nextcloud.example.com`.
### Mount Point {#services-nextcloud-server-mount-point}
If the `dataDir` exists in a mount point, it is highly recommended to make the various Nextcloud
services wait on the mount point before starting. Doing that is just a matter of setting the `mountPointServices` option.
Assuming a mount point on `/var`, the configuration would look like so:
```nix
fileSystems."/var".device = "...";
shb.nextcloud.mountPointServices = [ "var.mount" ];
```
### With LDAP Support {#services-nextcloud-server-usage-ldap}
:::: {.note}
@ -281,6 +293,15 @@ Note that you still need to generate the previews for any pre-existing files wit
nextcloud-occ -vvv preview:generate-all
```
The default settings generates all possible sizes which is a waste since most are not used. SHB will
change the generation settings to optimize disk space and CPU usage as outlined in [this
article](http://web.archive.org/web/20200513043150/https://ownyourbits.com/2019/06/29/understanding-and-improving-nextcloud-previews/).
You can opt-out with:
```nix
shb.nextcloud.apps.previewgenerator.recommendedSettings = false;
```
### Enable OnlyOffice App {#services-nextcloud-server-usage-onlyoffice}
The following snippet installs and enables the [Only
@ -322,6 +343,31 @@ See [my blog
post](http://blog.tiserbox.com/posts/2023-08-12-what%27s-up-with-nextcloud-webdav-slowness.html) for
how to look at the traces.
### Appdata Location {#services-nextcloud-server-server-usage-appdata}
The appdata folder is a special folder located under the `shb.nextcloud.dataDir` directory. It is
named `appdata_<instanceid>` with the Nextcloud's instance ID as a suffix. You can find your current
instance ID with `nextcloud-occ config:system:get instanceid`. In there, you will find one subfolder
for every installed app that needs to store files.
For performance reasons, it is recommended to store this folder on a fast drive that is optimized
for randomized read and write access. The best would be either an SSD or an NVMe drive.
If you intentionally put Nextcloud's `shb.nextcloud.dataDir` folder on a HDD with spinning disks,
for example because they offer more disk space, then the appdata folder is also located on spinning
drives. You are thus faced with a conundrum. The only way to solve this is to bind mount a folder
from an SSD over the appdata folder. SHB does not provide (yet?) a declarative way to setup this but
this command should be enough:
```bash
mount /dev/sdd /srv/sdd
mkdir -p /srv/sdd/appdata_nextcloud
mount --bind /srv/sdd/appdata_nextcloud /var/lib/nextcloud/data/appdata_ocxvky2f5ix7
```
Note that you can re-generate a new appdata folder by issuing the command `occ config:system:delete
instanceid`.
## Demo {#services-nextcloud-server-demo}
Head over to the [Nextcloud demo](demo-nextcloud-server.html) for a demo that installs Nextcloud with or

View file

@ -148,16 +148,15 @@ in
"f /var/lib/bitwarden_rs/vaultwarden.env 0640 vaultwarden vaultwarden"
];
systemd.services.vaultwarden.preStart =
let
envFile = pkgs.writeText "vaultwarden.env" ''
DATABASE_URL=postgresql://vaultwarden:%DB_PASSWORD%@127.0.0.1:5432/vaultwarden
SMTP_PASSWORD=%SMTP_PASSWORD%
'';
in
shblib.template envFile "/var/lib/bitwarden_rs/vaultwarden.env" {
"%DB_PASSWORD%" = "$(cat ${cfg.databasePasswordFile})";
"%SMTP_PASSWORD%" = "$(cat ${cfg.smtp.passwordFile})";
shblib.replaceSecrets {
userConfig = {
DATABASE_URL.source = cfg.databasePasswordFile;
DATABASE_URL.transform = v: "postgresql://vaultwarden:${v}@127.0.0.1:5432/vaultwarden";
SMTP_PASSWORD.source = cfg.smtp.passwordFile;
};
resultPath = "/var/lib/bitwarden_rs/vaultwarden.env";
generator = v: lib.generators.toINIWithGlobalSection {} { globalSection = v; };
};
shb.nginx.autheliaProtect = [
{

110
test/modules/lib.nix Normal file
View file

@ -0,0 +1,110 @@
{ pkgs, lib, ... }:
let
shblib = pkgs.callPackage ../../lib {};
in
{
# Tests that withReplacements can:
# - recurse in attrs and lists
# - .source field is understood
# - .transform field is understood
# - if .source field is found, ignores other fields
testLibWithReplacements = {
expected =
let
item = root: {
a = "A";
b = "%SECRET_${root}B%";
c = "%SECRET_${root}C%";
};
in
(item "") // {
nestedAttr = item "NESTEDATTR_";
nestedList = [ (item "NESTEDLIST_0_") ];
doubleNestedList = [ { n = (item "DOUBLENESTEDLIST_0_N_"); } ];
};
expr =
let
item = {
a = "A";
b.source = "/path/B";
b.transform = null;
c.source = "/path/C";
c.transform = v: "prefix-${v}-suffix";
c.other = "other";
};
in
shblib.withReplacements (
item // {
nestedAttr = item;
nestedList = [ item ];
doubleNestedList = [ { n = item; } ];
}
);
};
testLibWithReplacementsRootList = {
expected =
let
item = root: {
a = "A";
b = "%SECRET_${root}B%";
c = "%SECRET_${root}C%";
};
in
[
(item "0_")
(item "1_")
[ (item "2_0_") ]
[ { n = (item "3_0_N_"); } ]
];
expr =
let
item = {
a = "A";
b.source = "/path/B";
b.transform = null;
c.source = "/path/C";
c.transform = v: "prefix-${v}-suffix";
c.other = "other";
};
in
shblib.withReplacements [
item
item
[ item ]
[ { n = item; } ]
];
};
testLibGetReplacements = {
expected =
let
secrets = root: {
"%SECRET_${root}B%" = "$(cat /path/B)";
"%SECRET_${root}C%" = "prefix-$(cat /path/C)-suffix";
};
in
(secrets "") //
(secrets "NESTEDATTR_") //
(secrets "NESTEDLIST_0_") //
(secrets "DOUBLENESTEDLIST_0_N_");
expr =
let
item = {
a = "A";
b.source = "/path/B";
b.transform = null;
c.source = "/path/C";
c.transform = v: "prefix-${v}-suffix";
c.other = "other";
};
in
shblib.getReplacements (
item // {
nestedAttr = item;
nestedList = [ item ];
doubleNestedList = [ { n = item; } ];
}
);
};
}

View file

@ -10,7 +10,6 @@ in
imports = [
{
options = {
shb.ssl.enable = lib.mkEnableOption "ssl";
shb.backup = lib.mkOption { type = lib.types.anything; };
};
}
@ -49,7 +48,7 @@ in
{
id = "client1";
description = "My Client 1";
secretFile = pkgs.writeText "secret" "mysecuresecret";
secret.source = pkgs.writeText "secret" "mysecuresecret";
public = false;
authorization_policy = "one_factor";
redirect_uris = [ "http://client1.machine/redirect" ];
@ -57,7 +56,7 @@ in
{
id = "client2";
description = "My Client 2";
secretFile = pkgs.writeText "secret" "myothersecret";
secret.source = pkgs.writeText "secret" "myothersecret";
public = false;
authorization_policy = "one_factor";
redirect_uris = [ "http://client2.machine/redirect" ];

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']}")
'';
};
}

82
test/vm/lib.nix Normal file
View file

@ -0,0 +1,82 @@
{ pkgs, lib, ... }:
let
shblib = pkgs.callPackage ../../lib {};
in
{
template =
let
aSecret = pkgs.writeText "a-secret.txt" "Secret of A";
bSecret = pkgs.writeText "b-secret.txt" "Secret of B";
userConfig = {
a.a.source = aSecret;
b.source = bSecret;
b.transform = v: "prefix-${v}-suffix";
c = "not secret C";
d.d = "not secret D";
};
wantedConfig = {
a.a = "Secret of A";
b = "prefix-Secret of B-suffix";
c = "not secret C";
d.d = "not secret D";
};
configWithTemplates = shblib.withReplacements userConfig;
nonSecretConfigFile = pkgs.writeText "config.yaml.template" (lib.generators.toJSON {} configWithTemplates);
replacements = shblib.getReplacements userConfig;
replaceInTemplate = shblib.replaceSecretsScript {
file = nonSecretConfigFile;
resultPath = "/var/lib/config.yaml";
inherit replacements;
};
replaceInTemplate2 = shblib.replaceSecrets {
inherit userConfig;
resultPath = "/var/lib/config2.yaml";
generator = lib.generators.toJSON {};
};
in
pkgs.nixosTest {
name = "lib-template";
nodes.machine = { config, pkgs, ... }:
{
imports = [
{
options = {
libtest.config = lib.mkOption {
type = lib.types.attrsOf (lib.types.oneOf [ lib.types.str shblib.secretFileType ]);
};
};
}
];
system.activationScripts = {
libtest = replaceInTemplate;
libtest2 = replaceInTemplate2;
};
};
testScript = { nodes, ... }: ''
import json
start_all()
wantedConfig = json.loads('${lib.generators.toJSON {} wantedConfig}')
gotConfig = json.loads(machine.succeed("cat /var/lib/config.yaml"))
gotConfig2 = json.loads(machine.succeed("cat /var/lib/config2.yaml"))
# For debugging purpose
print(machine.succeed("cat ${pkgs.writeText "replaceInTemplate" replaceInTemplate}"))
print(machine.succeed("cat ${pkgs.writeText "replaceInTemplate2" replaceInTemplate2}"))
if wantedConfig != gotConfig:
raise Exception("\nwantedConfig: {}\n!= gotConfig: {}".format(wantedConfig, gotConfig))
if wantedConfig != gotConfig2:
raise Exception("\nwantedConfig: {}\n!= gotConfig2: {}".format(wantedConfig, gotConfig))
'';
};
}