From 6d49b12f80fb1335b8e89baf5f0187eb2a613f0f Mon Sep 17 00:00:00 2001 From: Tim Van Baak Date: Fri, 29 Nov 2024 11:26:07 -0800 Subject: [PATCH] nixos/immich-public-proxy: init module --- .../manual/release-notes/rl-2505.section.md | 2 + nixos/modules/module-list.nix | 1 + .../services/web-apps/immich-public-proxy.nix | 86 ++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/web-apps/immich-public-proxy.nix | 98 +++++++++++++++++++ .../im/immich-public-proxy/package.nix | 5 + 6 files changed, 193 insertions(+) create mode 100644 nixos/modules/services/web-apps/immich-public-proxy.nix create mode 100644 nixos/tests/web-apps/immich-public-proxy.nix diff --git a/nixos/doc/manual/release-notes/rl-2505.section.md b/nixos/doc/manual/release-notes/rl-2505.section.md index 89e43abc584cf4..3b834bd15b13b7 100644 --- a/nixos/doc/manual/release-notes/rl-2505.section.md +++ b/nixos/doc/manual/release-notes/rl-2505.section.md @@ -22,6 +22,8 @@ - [agorakit](https://github.com/agorakit/agorakit), an organization tool for citizens' collectives. Available with [services.agorakit](options.html#opt-services.agorakit.enable). +- [immich-public-proxy](https://github.com/alangrainger/immich-public-proxy), a proxy for sharing Immich albums without exposing the Immich API. Available as [services.immich-public-proxy](#opt-services.immich-public-proxy.enable). + - [mqtt-exporter](https://github.com/kpetremann/mqtt-exporter/), a Prometheus exporter for exposing messages from MQTT. Available as [services.prometheus.exporters.mqtt](#opt-services.prometheus.exporters.mqtt.enable). - [Buffyboard](https://gitlab.postmarketos.org/postmarketOS/buffybox/-/tree/master/buffyboard), a framebuffer on-screen keyboard. Available as [services.buffyboard](option.html#opt-services.buffyboard). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index afed4b049ebd03..c34a387bad54d5 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1463,6 +1463,7 @@ ./services/web-apps/icingaweb2/module-monitoring.nix ./services/web-apps/ifm.nix ./services/web-apps/immich.nix + ./services/web-apps/immich-public-proxy.nix ./services/web-apps/invidious.nix ./services/web-apps/invoiceplane.nix ./services/web-apps/isso.nix diff --git a/nixos/modules/services/web-apps/immich-public-proxy.nix b/nixos/modules/services/web-apps/immich-public-proxy.nix new file mode 100644 index 00000000000000..80c41c3926ab57 --- /dev/null +++ b/nixos/modules/services/web-apps/immich-public-proxy.nix @@ -0,0 +1,86 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.services.immich-public-proxy; + inherit (lib) + types + mkIf + mkOption + mkEnableOption + ; +in +{ + options.services.immich-public-proxy = { + enable = mkEnableOption "Immich Public Proxy"; + package = lib.mkPackageOption pkgs "immich-public-proxy" { }; + + immich-url = mkOption { + type = types.str; + description = "URL of the Immich instance"; + }; + + port = mkOption { + type = types.port; + default = 3000; + description = "The port that IPP will listen on."; + }; + openFirewall = mkOption { + type = types.bool; + default = false; + description = "Whether to open the IPP port in the firewall"; + }; + }; + + config = mkIf cfg.enable { + systemd.services.immich-public-proxy = { + description = "Immich public proxy for sharing albums publicly without exposing your Immich instance"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + environment = { + IMMICH_URL = cfg.immich-url; + IPP_PORT = builtins.toString cfg.port; + }; + serviceConfig = { + ExecStart = lib.getExe cfg.package; + SyslogIdentifier = "ipp"; + User = "ipp"; + Group = "ipp"; + DynamicUser = true; + Type = "simple"; + Restart = "on-failure"; + RestartSec = 3; + + # Hardening + CapabilityBoundingSet = ""; + NoNewPrivileges = true; + PrivateUsers = true; + PrivateTmp = true; + PrivateDevices = true; + PrivateMounts = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + }; + }; + + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [ cfg.port ]; + + meta.maintainers = with lib.maintainers; [ jaculabilis ]; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index e747e32a775c7b..e8e101a92f81a5 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -466,6 +466,7 @@ in { ifm = handleTest ./ifm.nix {}; iftop = handleTest ./iftop.nix {}; immich = handleTest ./web-apps/immich.nix {}; + immich-public-proxy = handleTest ./web-apps/immich-public-proxy.nix {}; incron = handleTest ./incron.nix {}; incus = pkgs.recurseIntoAttrs (handleTest ./incus { inherit handleTestOn; inherit (pkgs) incus; }); incus-lts = pkgs.recurseIntoAttrs (handleTest ./incus { inherit handleTestOn; }); diff --git a/nixos/tests/web-apps/immich-public-proxy.nix b/nixos/tests/web-apps/immich-public-proxy.nix new file mode 100644 index 00000000000000..f07a050f86d014 --- /dev/null +++ b/nixos/tests/web-apps/immich-public-proxy.nix @@ -0,0 +1,98 @@ +import ../make-test-python.nix ( + { pkgs, lib, ... }: + { + name = "immich-public-proxy"; + + nodes.machine = + { pkgs, ... }@args: + { + environment.systemPackages = [ + pkgs.imagemagick + pkgs.immich-cli + ]; + services.immich = { + enable = true; + port = 2283; + # disable a lot of features that aren't needed for this test + machine-learning.enable = false; + settings = { + backup.database.enabled = false; + machineLearning.enabled = false; + map.enabled = false; + reverseGeocoding.enabled = false; + metadata.faces.import = false; + newVersionCheck.enabled = false; + notifications.smtp.enabled = false; + }; + }; + services.immich-public-proxy = { + enable = true; + immich-url = "http://localhost:2283"; + port = 8002; + }; + }; + + testScript = '' + import json + + machine.wait_for_unit("immich-server.service") + machine.wait_for_unit("immich-public-proxy.service") + machine.wait_for_open_port(2283) + machine.wait_for_open_port(8002) + + # The proxy should be up + machine.succeed("curl -sf http://localhost:8002") + + # Verify the static assets are served + machine.succeed("curl -sf http://localhost:8002/robots.txt") + machine.succeed("curl -sf http://localhost:8002/share/static/style.css") + + # Log in to Immich and create an access key + machine.succeed(""" + curl -sf --json '{ "email": "test@example.com", "name": "Admin", "password": "admin" }' http://localhost:2283/api/auth/admin-sign-up + """) + res = machine.succeed(""" + curl -sf --json '{ "email": "test@example.com", "password": "admin" }' http://localhost:2283/api/auth/login + """) + token = json.loads(res)['accessToken'] + res = machine.succeed(""" + curl -sf -H 'Cookie: immich_access_token=%s' --json '{ "name": "API Key", "permissions": ["all"] }' http://localhost:2283/api/api-keys + """ % token) + key = json.loads(res)['secret'] + machine.succeed(f"immich login http://localhost:2283/api {key}") + res = machine.succeed("immich server-info") + print(res) + + # Upload some blank images to a new album + # If there's only one image, the proxy serves the image directly + machine.succeed("magick -size 800x600 canvas:white /tmp/white.png") + machine.succeed("immich upload -A '✨ Reproducible Moments ✨' /tmp/white.png") + machine.succeed("magick -size 800x600 canvas:black /tmp/black.png") + machine.succeed("immich upload -A '✨ Reproducible Moments ✨' /tmp/black.png") + res = machine.succeed("immich server-info") + print(res) + + # Get the new album id + res = machine.succeed(""" + curl -sf -H 'Cookie: immich_access_token=%s' http://localhost:2283/api/albums + """ % token) + album_id = json.loads(res)[0]['id'] + + # Create a shared link + res = machine.succeed(""" + curl -sf -H 'Cookie: immich_access_token=%s' --json '{ "albumId": "%s", "type": "ALBUM" }' http://localhost:2283/api/shared-links + """ % (token, album_id)) + share_key = json.loads(res)['key'] + + # Access the share + machine.succeed(""" + curl -sf http://localhost:2283/share/%s + """ % share_key) + + # Access the share through the proxy + machine.succeed(""" + curl -sf http://localhost:8002/share/%s + """ % share_key) + ''; + } +) diff --git a/pkgs/by-name/im/immich-public-proxy/package.nix b/pkgs/by-name/im/immich-public-proxy/package.nix index 2e092f619955c8..e054ccc603e6c7 100644 --- a/pkgs/by-name/im/immich-public-proxy/package.nix +++ b/pkgs/by-name/im/immich-public-proxy/package.nix @@ -2,6 +2,7 @@ lib, buildNpmPackage, fetchFromGitHub, + nixosTests, nodejs, }: buildNpmPackage rec { @@ -31,6 +32,10 @@ buildNpmPackage rec { "static('$out/lib/node_modules/immich-public-proxy/public'" ''; + passthru.tests = { + inherit (nixosTests) immich-public-proxy; + }; + meta = { description = "Share your Immich photos and albums in a safe way without exposing your Immich instance to the public"; homepage = "https://github.com/alangrainger/immich-public-proxy";