Merge branch 'main' into audiobookshelf
This commit is contained in:
commit
3bf84c732e
17 changed files with 1045 additions and 175 deletions
|
|
@ -230,21 +230,16 @@ SOPS_AGE_KEY_FILE=keys.txt nix run --impure nixpkgs#sops -- \
|
||||||
The `secrets.yaml` file must follow the format:
|
The `secrets.yaml` file must follow the format:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
home-assistant: |
|
home-assistant:
|
||||||
name: "My Instance"
|
|
||||||
country: "US"
|
country: "US"
|
||||||
latitude_home: "0.100"
|
latitude: "0.100"
|
||||||
longitude_home: "-0.100"
|
longitude: "-0.100"
|
||||||
time_zone: "America/Los_Angeles"
|
time_zone: "America/Los_Angeles"
|
||||||
unit_system: "metric"
|
|
||||||
lldap:
|
lldap:
|
||||||
user_password: XXX...
|
user_password: XXX...
|
||||||
jwt_secret: YYY...
|
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:
|
You can generate random secrets with:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
38
demo/homeassistant/flake.lock
generated
38
demo/homeassistant/flake.lock
generated
|
|
@ -5,11 +5,11 @@
|
||||||
"systems": "systems"
|
"systems": "systems"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1705309234,
|
"lastModified": 1709126324,
|
||||||
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -35,11 +35,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1707092692,
|
"lastModified": 1709150264,
|
||||||
"narHash": "sha256-ZbHsm+mGk/izkWtT4xwwqz38fdlwu7nUUKXTOmm4SyE=",
|
"narHash": "sha256-HofykKuisObPUfj0E9CJVfaMhawXkYx3G8UIFR/XQ38=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "faf912b086576fd1a15fca610166c98d47bc667e",
|
"rev": "9099616b93301d5cf84274b184a3a5ec69e94e08",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -51,27 +51,27 @@
|
||||||
},
|
},
|
||||||
"nixpkgs-stable": {
|
"nixpkgs-stable": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1705957679,
|
"lastModified": 1708819810,
|
||||||
"narHash": "sha256-Q8LJaVZGJ9wo33wBafvZSzapYsjOaNjP/pOnSiKVGHY=",
|
"narHash": "sha256-1KosU+ZFXf31GPeCBNxobZWMgHsSOJcrSFA6F2jhzdE=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "9a333eaa80901efe01df07eade2c16d183761fa3",
|
"rev": "89a2a12e6c8c6a56c72eb3589982c8e2f89c70ea",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"ref": "release-23.05",
|
"ref": "release-23.11",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1706925685,
|
"lastModified": 1708751719,
|
||||||
"narHash": "sha256-hVInjWMmgH4yZgA4ZtbgJM1qEAel72SYhP5nOWX4UIM=",
|
"narHash": "sha256-0uWOKSpXJXmXswOvDM5Vk3blB74apFB6rNGWV5IjoN0=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "79a13f1437e149dc7be2d1290c74d378dad60814",
|
"rev": "f63ce824cd2f036216eb5f637dfef31e1a03ee89",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -111,11 +111,11 @@
|
||||||
"sops-nix": "sops-nix"
|
"sops-nix": "sops-nix"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1707374005,
|
"lastModified": 1709267447,
|
||||||
"narHash": "sha256-W3p8hBLUdlHAG7yxT250jImnFmXe83tN119/jRiBYdo=",
|
"narHash": "sha256-5Q467FhpS18L/+5iB3wsWaR9tBqdzNt0fpdkZJNqNxc=",
|
||||||
"owner": "ibizaman",
|
"owner": "ibizaman",
|
||||||
"repo": "selfhostblocks",
|
"repo": "selfhostblocks",
|
||||||
"rev": "7d0276e9f2509bc6f175358c318374fedfc64422",
|
"rev": "fa206d0e1515fb0e49393e7ada6d7e5c6ec1df58",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -130,11 +130,11 @@
|
||||||
"nixpkgs-stable": "nixpkgs-stable"
|
"nixpkgs-stable": "nixpkgs-stable"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1707015547,
|
"lastModified": 1708987867,
|
||||||
"narHash": "sha256-YZr0OrqWPdbwBhxpBu69D32ngJZw8AMgZtJeaJn0e94=",
|
"narHash": "sha256-k2lDaDWNTU5sBVHanYzjDKVDmk29RHIgdbbXu5sdzBA=",
|
||||||
"owner": "Mic92",
|
"owner": "Mic92",
|
||||||
"repo": "sops-nix",
|
"repo": "sops-nix",
|
||||||
"rev": "23f61b897c00b66855074db471ba016e0cda20dd",
|
"rev": "a1c8de14f60924fafe13aea66b46157f0150f4cf",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,42 @@
|
||||||
enable = true;
|
enable = true;
|
||||||
domain = "example.com";
|
domain = "example.com";
|
||||||
subdomain = "ha";
|
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;
|
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 = [
|
nixpkgs.config.permittedInsecurePackages = [
|
||||||
|
|
|
||||||
|
|
@ -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:
|
lldap:
|
||||||
user_password: ENC[AES256_GCM,data:JrFraqFSqAhRVjB5fagIoB864aejt24q+qqWeu8ySC0=,iv:RS7VS+9tsSknn9SwpfyYVi41m3lN4SkZ4CSwrzH/Eso=,tag:5L7fx6/KhDtjHPruwac/sw==,type:str]
|
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]
|
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
|
VlJpS1BYd2UrZU1mZTEwU1BYODhqM2sKvQnFV8xsy1tEmYZu4izBYb7XQqTPOLTL
|
||||||
bRkU6n17uiyXNbiXDAbX0Png/XmVG96/+Zl38BBXPQvARX8c2tzq6w==
|
bRkU6n17uiyXNbiXDAbX0Png/XmVG96/+Zl38BBXPQvARX8c2tzq6w==
|
||||||
-----END AGE ENCRYPTED FILE-----
|
-----END AGE ENCRYPTED FILE-----
|
||||||
lastmodified: "2024-01-23T00:46:58Z"
|
lastmodified: "2024-02-12T05:07:51Z"
|
||||||
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]
|
mac: ENC[AES256_GCM,data:MOmvK0g6Wj+fND154QUhmXujsDOKMO5CRRckru+eDRPeHcJZUnI/jjolcI8y+LEdhUVf0Ln8E38GSxZT/8EW3CfCNkOUikGFdfxuQ2uzNp/1wMvNaF988lrXMBfQ7Il18AiYVK0QhGReGXJa6wBVUb2Qfrg41WC65UvQtMOByqI=,iv:Rscvq1l7YgNapC0NkabQHBzirzsPEr8ykAQqx+qGoi0=,tag:ud+K72bnUV1hnsjcewNrsw==,type:str]
|
||||||
pgp: []
|
pgp: []
|
||||||
unencrypted_suffix: _unencrypted
|
unencrypted_suffix: _unencrypted
|
||||||
version: 3.8.1
|
version: 3.8.1
|
||||||
|
|
|
||||||
30
flake.lock
generated
30
flake.lock
generated
|
|
@ -5,11 +5,11 @@
|
||||||
"systems": "systems"
|
"systems": "systems"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1705309234,
|
"lastModified": 1709126324,
|
||||||
"narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=",
|
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "flake-utils",
|
"repo": "flake-utils",
|
||||||
"rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26",
|
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -35,11 +35,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1707956935,
|
"lastModified": 1709237383,
|
||||||
"narHash": "sha256-ZL2TrjVsiFNKOYwYQozpbvQSwvtV/3Me7Zwhmdsfyu4=",
|
"narHash": "sha256-cy6ArO4k5qTx+l5o+0mL9f5fa86tYUX3ozE1S+Txlds=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "a4d4fe8c5002202493e87ec8dbc91335ff55552c",
|
"rev": "1536926ef5621b09bba54035ae2bb6d806d72ac8",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -51,11 +51,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs-stable": {
|
"nixpkgs-stable": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1707603439,
|
"lastModified": 1708819810,
|
||||||
"narHash": "sha256-LodBVZ3+ehJP2azM5oj+JrhfNAAzmTJ/OwAIOn0RfZ0=",
|
"narHash": "sha256-1KosU+ZFXf31GPeCBNxobZWMgHsSOJcrSFA6F2jhzdE=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "d8cd80616c8800feec0cab64331d7c3d5a1a6d98",
|
"rev": "89a2a12e6c8c6a56c72eb3589982c8e2f89c70ea",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -67,11 +67,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1707451808,
|
"lastModified": 1708751719,
|
||||||
"narHash": "sha256-UwDBUNHNRsYKFJzyTMVMTF5qS4xeJlWoeyJf+6vvamU=",
|
"narHash": "sha256-0uWOKSpXJXmXswOvDM5Vk3blB74apFB6rNGWV5IjoN0=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "442d407992384ed9c0e6d352de75b69079904e4e",
|
"rev": "f63ce824cd2f036216eb5f637dfef31e1a03ee89",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -112,11 +112,11 @@
|
||||||
"nixpkgs-stable": "nixpkgs-stable"
|
"nixpkgs-stable": "nixpkgs-stable"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1707842202,
|
"lastModified": 1708987867,
|
||||||
"narHash": "sha256-3dTBbCzHJBinwhsisGJHW1HLBsLbj91+a5ZDXt7ttW0=",
|
"narHash": "sha256-k2lDaDWNTU5sBVHanYzjDKVDmk29RHIgdbbXu5sdzBA=",
|
||||||
"owner": "Mic92",
|
"owner": "Mic92",
|
||||||
"repo": "sops-nix",
|
"repo": "sops-nix",
|
||||||
"rev": "48afd3264ec52bee85231a7122612e2c5202fa74",
|
"rev": "a1c8de14f60924fafe13aea66b46157f0150f4cf",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
|
|
@ -89,14 +89,22 @@
|
||||||
mergeTests (importFiles [
|
mergeTests (importFiles [
|
||||||
./test/modules/arr.nix
|
./test/modules/arr.nix
|
||||||
./test/modules/davfs.nix
|
./test/modules/davfs.nix
|
||||||
|
./test/modules/lib.nix
|
||||||
./test/modules/nginx.nix
|
./test/modules/nginx.nix
|
||||||
./test/modules/postgresql.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 "audiobookshelf" ./test/vm/audiobookshelf.nix)
|
||||||
// (vm_test "authelia" ./test/vm/authelia.nix)
|
// (vm_test "authelia" ./test/vm/authelia.nix)
|
||||||
|
// (vm_test "jellyfin" ./test/vm/jellyfin.nix)
|
||||||
// (vm_test "ldap" ./test/vm/ldap.nix)
|
// (vm_test "ldap" ./test/vm/ldap.nix)
|
||||||
|
// (vm_test "lib" ./test/vm/lib.nix)
|
||||||
// (vm_test "postgresql" ./test/vm/postgresql.nix)
|
// (vm_test "postgresql" ./test/vm/postgresql.nix)
|
||||||
// (vm_test "monitoring" ./test/vm/monitoring.nix)
|
// (vm_test "monitoring" ./test/vm/monitoring.nix)
|
||||||
// (vm_test "nextcloud" ./test/vm/nextcloud.nix)
|
// (vm_test "nextcloud" ./test/vm/nextcloud.nix)
|
||||||
|
|
|
||||||
109
lib/default.nix
109
lib/default.nix
|
|
@ -1,13 +1,110 @@
|
||||||
{ lib }:
|
{ pkgs, lib }:
|
||||||
{
|
rec {
|
||||||
template = file: newPath: replacements:
|
replaceSecrets = { userConfig, resultPath, generator }:
|
||||||
let
|
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);
|
sedPatterns = lib.strings.concatStringsSep " " (lib.attrsets.mapAttrsToList (from: to: "-e \"s|${from}|${to}|\"") replacements);
|
||||||
in
|
in
|
||||||
''
|
''
|
||||||
|
set -euo pipefail
|
||||||
|
set -x
|
||||||
|
mkdir -p $(dirname ${templatePath})
|
||||||
ln -fs ${file} ${templatePath}
|
ln -fs ${file} ${templatePath}
|
||||||
rm ${newPath} || :
|
rm -f ${resultPath}
|
||||||
sed ${sedPatterns} ${templatePath} > ${newPath}
|
${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
|
||||||
|
[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -94,9 +94,54 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
oidcClients = lib.mkOption {
|
oidcClients = lib.mkOption {
|
||||||
type = lib.types.listOf lib.types.anything;
|
|
||||||
description = "OIDC clients";
|
description = "OIDC clients";
|
||||||
default = [];
|
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 {
|
smtp = lib.mkOption {
|
||||||
|
|
@ -291,13 +336,13 @@ in
|
||||||
systemd.services."authelia-${fqdn}".preStart =
|
systemd.services."authelia-${fqdn}".preStart =
|
||||||
let
|
let
|
||||||
mkCfg = clients:
|
mkCfg = clients:
|
||||||
let
|
shblib.replaceSecrets {
|
||||||
addTemplate = client: (builtins.removeAttrs client ["secretFile"]) // {secret = "%SECRET_${client.id}%";};
|
userConfig = {
|
||||||
tmplFile = pkgs.writeText "oidc_clients.yaml" (lib.generators.toYAML {} {identity_providers.oidc.clients = map addTemplate clients;});
|
identity_providers.oidc.clients = clients;
|
||||||
replace = client: {"%SECRET_${client.id}%" = "$(cat ${toString client.secretFile})";};
|
};
|
||||||
replacements = lib.foldl (container: client: container // (replace client) ) {} clients;
|
resultPath = "/var/lib/authelia-${fqdn}/oidc_clients.yaml";
|
||||||
in
|
generator = lib.generators.toYAML {};
|
||||||
shblib.template tmplFile "/var/lib/authelia-${fqdn}/oidc_clients.yaml" replacements;
|
};
|
||||||
in
|
in
|
||||||
lib.mkBefore (mkCfg cfg.oidcClients);
|
lib.mkBefore (mkCfg cfg.oidcClients);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ let
|
||||||
cfg = config.shb.home-assistant;
|
cfg = config.shb.home-assistant;
|
||||||
|
|
||||||
contracts = pkgs.callPackage ../contracts {};
|
contracts = pkgs.callPackage ../contracts {};
|
||||||
|
shblib = pkgs.callPackage ../../lib {};
|
||||||
|
|
||||||
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
fqdn = "${cfg.subdomain}.${cfg.domain}";
|
||||||
|
|
||||||
|
|
@ -18,6 +19,15 @@ let
|
||||||
export PATH=${pkgs.gnused}/bin:${pkgs.curl}/bin:${pkgs.jq}/bin
|
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 $@
|
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
|
in
|
||||||
{
|
{
|
||||||
options.shb.home-assistant = {
|
options.shb.home-assistant = {
|
||||||
|
|
@ -41,6 +51,41 @@ in
|
||||||
default = null;
|
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 {
|
ldap = lib.mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
LDAP Integration App. [Manual](https://docs.nextcloud.com/server/latest/admin_manual/configuration_user/user_auth_ldap.html)
|
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 {
|
backupCfg = lib.mkOption {
|
||||||
type = lib.types.anything;
|
type = lib.types.anything;
|
||||||
description = "Backup configuration for home-assistant";
|
description = "Backup configuration for home-assistant";
|
||||||
|
|
@ -144,14 +183,8 @@ in
|
||||||
trusted_proxies = "127.0.0.1";
|
trusted_proxies = "127.0.0.1";
|
||||||
};
|
};
|
||||||
logger.default = "info";
|
logger.default = "info";
|
||||||
homeassistant = {
|
homeassistant = configWithSecretsIncludes // {
|
||||||
external_url = "https://${cfg.subdomain}.${cfg.domain}";
|
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 =
|
auth_providers =
|
||||||
(lib.optionals (!cfg.ldap.enable || cfg.ldap.keepDefaultAuth) [
|
(lib.optionals (!cfg.ldap.enable || cfg.ldap.keepDefaultAuth) [
|
||||||
{
|
{
|
||||||
|
|
@ -256,23 +289,18 @@ in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
storage = "${config.services.home-assistant.configDir}/.storage";
|
storage = "${config.services.home-assistant.configDir}";
|
||||||
file = "${storage}/onboarding";
|
file = "${storage}/.storage/onboarding";
|
||||||
in
|
in
|
||||||
''
|
''
|
||||||
if ! -f ${file}; then
|
if ! -f ${file}; then
|
||||||
mkdir -p ${storage} && cp ${onboarding} ${file}
|
mkdir -p ${storage} && cp ${onboarding} ${file}
|
||||||
fi
|
fi
|
||||||
'');
|
'' + shblib.replaceSecrets {
|
||||||
|
userConfig = cfg.config;
|
||||||
sops.secrets."home-assistant" = {
|
resultPath = "${config.services.home-assistant.configDir}/secrets.yaml";
|
||||||
inherit (cfg) sopsFile;
|
generator = lib.generators.toYAML {};
|
||||||
mode = "0440";
|
});
|
||||||
owner = "hass";
|
|
||||||
group = "hass";
|
|
||||||
path = "${config.services.home-assistant.configDir}/secrets.yaml";
|
|
||||||
restartUnits = [ "home-assistant.service" ];
|
|
||||||
};
|
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
systemd.tmpfiles.rules = [
|
||||||
"f ${config.services.home-assistant.configDir}/automations.yaml 0755 hass hass"
|
"f ${config.services.home-assistant.configDir}/automations.yaml 0755 hass hass"
|
||||||
|
|
|
||||||
|
|
@ -30,62 +30,94 @@ in
|
||||||
default = null;
|
default = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
ldapHost = lib.mkOption {
|
ldap = lib.mkOption {
|
||||||
|
description = "LDAP configuration.";
|
||||||
|
default = {};
|
||||||
|
type = lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
enable = lib.mkEnableOption "LDAP";
|
||||||
|
|
||||||
|
host = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "host serving the LDAP server";
|
description = "Host serving the LDAP server.";
|
||||||
example = "127.0.0.1";
|
example = "127.0.0.1";
|
||||||
};
|
};
|
||||||
|
|
||||||
ldapPort = lib.mkOption {
|
port = lib.mkOption {
|
||||||
type = lib.types.int;
|
type = lib.types.int;
|
||||||
description = "port where the LDAP server is listening";
|
description = "Port where the LDAP server is listening.";
|
||||||
example = 389;
|
example = 389;
|
||||||
};
|
};
|
||||||
|
|
||||||
dcdomain = lib.mkOption {
|
dcdomain = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "dc domain for ldap";
|
description = "DC domain for LDAP.";
|
||||||
example = "dc=mydomain,dc=com";
|
example = "dc=mydomain,dc=com";
|
||||||
};
|
};
|
||||||
|
|
||||||
oidcProvider = lib.mkOption {
|
userGroup = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "LDAP user group";
|
||||||
|
default = "jellyfin_user";
|
||||||
|
};
|
||||||
|
|
||||||
|
adminGroup = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
description = "LDAP admin group";
|
||||||
|
default = "jellyfin_admin";
|
||||||
|
};
|
||||||
|
|
||||||
|
passwordFile = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
description = "File containing the LDAP admin password.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
sso = lib.mkOption {
|
||||||
|
description = "SSO configuration.";
|
||||||
|
default = {};
|
||||||
|
type = lib.types.submodule {
|
||||||
|
options = {
|
||||||
|
enable = lib.mkEnableOption "SSO";
|
||||||
|
|
||||||
|
provider = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "OIDC provider name";
|
description = "OIDC provider name";
|
||||||
default = "Authelia";
|
default = "Authelia";
|
||||||
};
|
};
|
||||||
|
|
||||||
authEndpoint = lib.mkOption {
|
endpoint = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "OIDC endpoint for SSO";
|
description = "OIDC endpoint for SSO";
|
||||||
example = "https://authelia.example.com";
|
example = "https://authelia.example.com";
|
||||||
};
|
};
|
||||||
|
|
||||||
oidcClientID = lib.mkOption {
|
clientID = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Client ID for the OIDC endpoint";
|
description = "Client ID for the OIDC endpoint";
|
||||||
default = "jellyfin";
|
default = "jellyfin";
|
||||||
};
|
};
|
||||||
|
|
||||||
oidcAdminUserGroup = lib.mkOption {
|
adminUserGroup = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "OIDC admin group";
|
description = "OIDC admin group";
|
||||||
default = "jellyfin_admin";
|
default = "jellyfin_admin";
|
||||||
};
|
};
|
||||||
|
|
||||||
oidcUserGroup = lib.mkOption {
|
userGroup = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "OIDC user group";
|
description = "OIDC user group";
|
||||||
default = "jellyfin_user";
|
default = "jellyfin_user";
|
||||||
};
|
};
|
||||||
|
|
||||||
ldapPasswordFile = lib.mkOption {
|
secretFile = lib.mkOption {
|
||||||
type = lib.types.path;
|
type = lib.types.path;
|
||||||
description = "File containing the LDAP admin password.";
|
description = "File containing the OIDC shared secret.";
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
ssoSecretFile = lib.mkOption {
|
|
||||||
type = lib.types.path;
|
|
||||||
description = "File containing the SSO shared secret.";
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -107,6 +139,8 @@ in
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
services.nginx.enable = true;
|
||||||
|
|
||||||
# Take advice from https://jellyfin.org/docs/general/networking/nginx/ and https://nixos.wiki/wiki/Plex
|
# Take advice from https://jellyfin.org/docs/general/networking/nginx/ and https://nixos.wiki/wiki/Plex
|
||||||
services.nginx.virtualHosts."${fqdn}" = {
|
services.nginx.virtualHosts."${fqdn}" = {
|
||||||
forceSSL = !(isNull cfg.ssl);
|
forceSSL = !(isNull cfg.ssl);
|
||||||
|
|
@ -238,17 +272,17 @@ in
|
||||||
ldapConfig = pkgs.writeText "LDAP-Auth.xml" ''
|
ldapConfig = pkgs.writeText "LDAP-Auth.xml" ''
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<PluginConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
<PluginConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||||
<LdapServer>${cfg.ldapHost}</LdapServer>
|
<LdapServer>${cfg.ldap.host}</LdapServer>
|
||||||
<LdapPort>${builtins.toString cfg.ldapPort}</LdapPort>
|
<LdapPort>${builtins.toString cfg.ldap.port}</LdapPort>
|
||||||
<UseSsl>false</UseSsl>
|
<UseSsl>false</UseSsl>
|
||||||
<UseStartTls>false</UseStartTls>
|
<UseStartTls>false</UseStartTls>
|
||||||
<SkipSslVerify>false</SkipSslVerify>
|
<SkipSslVerify>false</SkipSslVerify>
|
||||||
<LdapBindUser>uid=admin,ou=people,${cfg.dcdomain}</LdapBindUser>
|
<LdapBindUser>uid=admin,ou=people,${cfg.ldap.dcdomain}</LdapBindUser>
|
||||||
<LdapBindPassword>%LDAP_PASSWORD%</LdapBindPassword>
|
<LdapBindPassword>%LDAP_PASSWORD%</LdapBindPassword>
|
||||||
<LdapBaseDn>ou=people,${cfg.dcdomain}</LdapBaseDn>
|
<LdapBaseDn>ou=people,${cfg.ldap.dcdomain}</LdapBaseDn>
|
||||||
<LdapSearchFilter>(memberof=cn=jellyfin_user,ou=groups,${cfg.dcdomain})</LdapSearchFilter>
|
<LdapSearchFilter>(memberof=cn=${cfg.ldap.userGroup},ou=groups,${cfg.ldap.dcdomain})</LdapSearchFilter>
|
||||||
<LdapAdminBaseDn>ou=people,${cfg.dcdomain}</LdapAdminBaseDn>
|
<LdapAdminBaseDn>ou=people,${cfg.ldap.dcdomain}</LdapAdminBaseDn>
|
||||||
<LdapAdminFilter>(memberof=cn=jellyfin_admin,ou=groups,${cfg.dcdomain})</LdapAdminFilter>
|
<LdapAdminFilter>(memberof=cn=${cfg.ldap.adminGroup},ou=groups,${cfg.ldap.dcdomain})</LdapAdminFilter>
|
||||||
<EnableLdapAdminFilterMemberUid>false</EnableLdapAdminFilterMemberUid>
|
<EnableLdapAdminFilterMemberUid>false</EnableLdapAdminFilterMemberUid>
|
||||||
<LdapSearchAttributes>uid, cn, mail, displayName</LdapSearchAttributes>
|
<LdapSearchAttributes>uid, cn, mail, displayName</LdapSearchAttributes>
|
||||||
<LdapClientCertPath />
|
<LdapClientCertPath />
|
||||||
|
|
@ -271,22 +305,22 @@ in
|
||||||
<OidConfigs>
|
<OidConfigs>
|
||||||
<item>
|
<item>
|
||||||
<key>
|
<key>
|
||||||
<string>${cfg.oidcProvider}</string>
|
<string>${cfg.sso.provider}</string>
|
||||||
</key>
|
</key>
|
||||||
<value>
|
<value>
|
||||||
<PluginConfiguration>
|
<PluginConfiguration>
|
||||||
<OidEndpoint>${cfg.authEndpoint}</OidEndpoint>
|
<OidEndpoint>${cfg.sso.endpoint}</OidEndpoint>
|
||||||
<OidClientId>${cfg.oidcClientID}</OidClientId>
|
<OidClientId>${cfg.sso.clientID}</OidClientId>
|
||||||
<OidSecret>%SSO_SECRET%</OidSecret>
|
<OidSecret>%SSO_SECRET%</OidSecret>
|
||||||
<Enabled>true</Enabled>
|
<Enabled>true</Enabled>
|
||||||
<EnableAuthorization>true</EnableAuthorization>
|
<EnableAuthorization>true</EnableAuthorization>
|
||||||
<EnableAllFolders>true</EnableAllFolders>
|
<EnableAllFolders>true</EnableAllFolders>
|
||||||
<EnabledFolders />
|
<EnabledFolders />
|
||||||
<AdminRoles>
|
<AdminRoles>
|
||||||
<string>${cfg.oidcAdminUserGroup}</string>
|
<string>${cfg.sso.adminUserGroup}</string>
|
||||||
</AdminRoles>
|
</AdminRoles>
|
||||||
<Roles>
|
<Roles>
|
||||||
<string>${cfg.oidcUserGroup}</string>
|
<string>${cfg.sso.userGroup}</string>
|
||||||
</Roles>
|
</Roles>
|
||||||
<EnableFolderRoles>false</EnableFolderRoles>
|
<EnableFolderRoles>false</EnableFolderRoles>
|
||||||
<FolderRoleMappings />
|
<FolderRoleMappings />
|
||||||
|
|
@ -305,15 +339,15 @@ in
|
||||||
brandingConfig = pkgs.writeText "branding.xml" ''
|
brandingConfig = pkgs.writeText "branding.xml" ''
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<BrandingOptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
<BrandingOptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
||||||
<LoginDisclaimer><a href="https://${cfg.subdomain}.${cfg.domain}/SSO/OID/p/${cfg.oidcProvider}" class="raised cancel block emby-button authentik-sso">
|
<LoginDisclaimer><a href="https://${cfg.subdomain}.${cfg.domain}/SSO/OID/p/${cfg.sso.provider}" class="raised cancel block emby-button authentik-sso">
|
||||||
Sign in with ${cfg.oidcProvider}&nbsp;
|
Sign in with ${cfg.sso.provider}&nbsp;
|
||||||
<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">
|
<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">
|
||||||
</a>
|
</a>
|
||||||
<a href="https://${cfg.subdomain}.${cfg.domain}/SSOViews/linking" class="raised cancel block emby-button authentik-sso">
|
<a href="https://${cfg.subdomain}.${cfg.domain}/SSOViews/linking" class="raised cancel block emby-button authentik-sso">
|
||||||
Link ${cfg.oidcProvider} config&nbsp;
|
Link ${cfg.sso.provider} config&nbsp;
|
||||||
</a>
|
</a>
|
||||||
<a href="${cfg.authEndpoint}" class="raised cancel block emby-button authentik-sso">
|
<a href="${cfg.sso.endpoint}" class="raised cancel block emby-button authentik-sso">
|
||||||
${cfg.oidcProvider} config&nbsp;
|
${cfg.sso.provider} config&nbsp;
|
||||||
</a>
|
</a>
|
||||||
</LoginDisclaimer>
|
</LoginDisclaimer>
|
||||||
<CustomCss>
|
<CustomCss>
|
||||||
|
|
@ -348,22 +382,36 @@ in
|
||||||
</BrandingOptions>
|
</BrandingOptions>
|
||||||
'';
|
'';
|
||||||
in
|
in
|
||||||
shblib.template ldapConfig "/var/lib/jellyfin/plugins/configurations/LDAP-Auth.xml" {
|
lib.strings.optionalString cfg.ldap.enable (shblib.replaceSecretsScript {
|
||||||
"%LDAP_PASSWORD%" = "$(cat ${cfg.ldapPasswordFile})";
|
file = ldapConfig;
|
||||||
}
|
resultPath = "/var/lib/jellyfin/plugins/configurations/LDAP-Auth.xml";
|
||||||
+ shblib.template ssoConfig "/var/lib/jellyfin/plugins/configurations/SSO-Auth.xml" {
|
replacements = {
|
||||||
"%SSO_SECRET%" = "$(cat ${cfg.ssoSecretFile})";
|
"%LDAP_PASSWORD%" = "$(cat ${cfg.ldap.passwordFile})";
|
||||||
}
|
};
|
||||||
+ shblib.template brandingConfig "/var/lib/jellyfin/config/branding.xml" {"%a%" = "%a%";};
|
})
|
||||||
|
+ 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";
|
description = "Jellyfin";
|
||||||
secretFile = cfg.ssoSecretFile;
|
secret.source = cfg.sso.secretFile;
|
||||||
public = false;
|
public = false;
|
||||||
authorization_policy = "one_factor";
|
authorization_policy = "one_factor";
|
||||||
redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/sso/OID/r/${cfg.oidcProvider}" ];
|
redirect_uris = [ "https://${cfg.subdomain}.${cfg.domain}/sso/OID/r/${cfg.sso.provider}" ];
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,13 @@ in
|
||||||
default = "/var/lib/nextcloud";
|
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 {
|
adminUser = lib.mkOption {
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Username of the initial admin user.";
|
description = "Username of the initial admin user.";
|
||||||
|
|
@ -239,6 +246,27 @@ in
|
||||||
options = {
|
options = {
|
||||||
enable = lib.mkEnableOption "Nextcloud Preview Generator App";
|
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 {
|
debug = lib.mkOption {
|
||||||
type = lib.types.bool;
|
type = lib.types.bool;
|
||||||
description = "Enable more verbose logging.";
|
description = "Enable more verbose logging.";
|
||||||
|
|
@ -595,10 +623,17 @@ in
|
||||||
systemd.services.phpfpm-nextcloud.preStart = ''
|
systemd.services.phpfpm-nextcloud.preStart = ''
|
||||||
mkdir -p /var/log/xdebug; chown -R nextcloud: /var/log/xdebug
|
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 = [
|
systemd.services.nextcloud-cron.path = [
|
||||||
pkgs.perl
|
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.
|
# Sets up backup for Nextcloud.
|
||||||
shb.backup.instances.nextcloud = {
|
shb.backup.instances.nextcloud = {
|
||||||
|
|
@ -649,10 +684,23 @@ in
|
||||||
inherit ((nextcloudApps cfg.version)) previewgenerator;
|
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
|
# Configured as defined in https://github.com/nextcloud/previewgenerator
|
||||||
systemd.timers.nextcloud-cron-previewgenerator = {
|
systemd.timers.nextcloud-cron-previewgenerator = {
|
||||||
wantedBy = [ "timers.target" ];
|
wantedBy = [ "timers.target" ];
|
||||||
after = [ "nextcloud-setup.service" ];
|
requires = cfg.mountPointServices;
|
||||||
|
after = [ "nextcloud-setup.service" ] + cfg.mountPointServices;
|
||||||
timerConfig.OnBootSec = "10m";
|
timerConfig.OnBootSec = "10m";
|
||||||
timerConfig.OnUnitActiveSec = "10m";
|
timerConfig.OnUnitActiveSec = "10m";
|
||||||
timerConfig.Unit = "nextcloud-cron-previewgenerator.service";
|
timerConfig.Unit = "nextcloud-cron-previewgenerator.service";
|
||||||
|
|
@ -829,8 +877,8 @@ in
|
||||||
{
|
{
|
||||||
id = cfg.apps.sso.clientID;
|
id = cfg.apps.sso.clientID;
|
||||||
description = "Nextcloud";
|
description = "Nextcloud";
|
||||||
secretFile = cfg.apps.sso.secretFileForAuthelia;
|
secret.source = cfg.apps.sso.secretFileForAuthelia;
|
||||||
public = "false";
|
public = false;
|
||||||
authorization_policy = cfg.apps.sso.authorization_policy;
|
authorization_policy = cfg.apps.sso.authorization_policy;
|
||||||
redirect_uris = [ "${protocol}://${fqdnWithPort}/apps/oidc_login/oidc" ];
|
redirect_uris = [ "${protocol}://${fqdnWithPort}/apps/oidc_login/oidc" ];
|
||||||
scopes = [
|
scopes = [
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,18 @@ shb.nextcloud = {
|
||||||
|
|
||||||
After deploying, the Nextcloud server will be reachable at `http://nextcloud.example.com`.
|
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}
|
### With LDAP Support {#services-nextcloud-server-usage-ldap}
|
||||||
|
|
||||||
:::: {.note}
|
:::: {.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
|
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}
|
### Enable OnlyOffice App {#services-nextcloud-server-usage-onlyoffice}
|
||||||
|
|
||||||
The following snippet installs and enables the [Only
|
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
|
post](http://blog.tiserbox.com/posts/2023-08-12-what%27s-up-with-nextcloud-webdav-slowness.html) for
|
||||||
how to look at the traces.
|
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}
|
## Demo {#services-nextcloud-server-demo}
|
||||||
|
|
||||||
Head over to the [Nextcloud demo](demo-nextcloud-server.html) for a demo that installs Nextcloud with or
|
Head over to the [Nextcloud demo](demo-nextcloud-server.html) for a demo that installs Nextcloud with or
|
||||||
|
|
|
||||||
|
|
@ -148,15 +148,14 @@ in
|
||||||
"f /var/lib/bitwarden_rs/vaultwarden.env 0640 vaultwarden vaultwarden"
|
"f /var/lib/bitwarden_rs/vaultwarden.env 0640 vaultwarden vaultwarden"
|
||||||
];
|
];
|
||||||
systemd.services.vaultwarden.preStart =
|
systemd.services.vaultwarden.preStart =
|
||||||
let
|
shblib.replaceSecrets {
|
||||||
envFile = pkgs.writeText "vaultwarden.env" ''
|
userConfig = {
|
||||||
DATABASE_URL=postgresql://vaultwarden:%DB_PASSWORD%@127.0.0.1:5432/vaultwarden
|
DATABASE_URL.source = cfg.databasePasswordFile;
|
||||||
SMTP_PASSWORD=%SMTP_PASSWORD%
|
DATABASE_URL.transform = v: "postgresql://vaultwarden:${v}@127.0.0.1:5432/vaultwarden";
|
||||||
'';
|
SMTP_PASSWORD.source = cfg.smtp.passwordFile;
|
||||||
in
|
};
|
||||||
shblib.template envFile "/var/lib/bitwarden_rs/vaultwarden.env" {
|
resultPath = "/var/lib/bitwarden_rs/vaultwarden.env";
|
||||||
"%DB_PASSWORD%" = "$(cat ${cfg.databasePasswordFile})";
|
generator = v: lib.generators.toINIWithGlobalSection {} { globalSection = v; };
|
||||||
"%SMTP_PASSWORD%" = "$(cat ${cfg.smtp.passwordFile})";
|
|
||||||
};
|
};
|
||||||
|
|
||||||
shb.nginx.autheliaProtect = [
|
shb.nginx.autheliaProtect = [
|
||||||
|
|
|
||||||
110
test/modules/lib.nix
Normal file
110
test/modules/lib.nix
Normal 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; } ];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,6 @@ in
|
||||||
imports = [
|
imports = [
|
||||||
{
|
{
|
||||||
options = {
|
options = {
|
||||||
shb.ssl.enable = lib.mkEnableOption "ssl";
|
|
||||||
shb.backup = lib.mkOption { type = lib.types.anything; };
|
shb.backup = lib.mkOption { type = lib.types.anything; };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -49,7 +48,7 @@ in
|
||||||
{
|
{
|
||||||
id = "client1";
|
id = "client1";
|
||||||
description = "My Client 1";
|
description = "My Client 1";
|
||||||
secretFile = pkgs.writeText "secret" "mysecuresecret";
|
secret.source = pkgs.writeText "secret" "mysecuresecret";
|
||||||
public = false;
|
public = false;
|
||||||
authorization_policy = "one_factor";
|
authorization_policy = "one_factor";
|
||||||
redirect_uris = [ "http://client1.machine/redirect" ];
|
redirect_uris = [ "http://client1.machine/redirect" ];
|
||||||
|
|
@ -57,7 +56,7 @@ in
|
||||||
{
|
{
|
||||||
id = "client2";
|
id = "client2";
|
||||||
description = "My Client 2";
|
description = "My Client 2";
|
||||||
secretFile = pkgs.writeText "secret" "myothersecret";
|
secret.source = pkgs.writeText "secret" "myothersecret";
|
||||||
public = false;
|
public = false;
|
||||||
authorization_policy = "one_factor";
|
authorization_policy = "one_factor";
|
||||||
redirect_uris = [ "http://client2.machine/redirect" ];
|
redirect_uris = [ "http://client2.machine/redirect" ];
|
||||||
|
|
|
||||||
326
test/vm/jellyfin.nix
Normal file
326
test/vm/jellyfin.nix
Normal 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
82
test/vm/lib.nix
Normal 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))
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue