diff --git a/nixos/doc/manual/redirects.json b/nixos/doc/manual/redirects.json index 88cb8f37f146d..532e10c2f2022 100644 --- a/nixos/doc/manual/redirects.json +++ b/nixos/doc/manual/redirects.json @@ -298,6 +298,9 @@ "module-services-crab-hole-upstream-options": [ "index.html#module-services-crab-hole-upstream-options" ], + "module-services-firefox-syncserver-database": [ + "index.html#module-services-firefox-syncserver-database" + ], "module-services-firefox-syncserver-clients": [ "index.html#module-services-firefox-syncserver-clients" ], diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index a652c7a3657c8..fd37e406a777f 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -407,6 +407,8 @@ See . - IPVLAN interfaces can now be configured through the `networking.ipvlans` option in the networking module. +- [](#opt-services.firefox-syncserver.database.type) has been added to allow choosing between MySQL/MariaDB (default) and PostgreSQL as the database backend for the Firefox Sync server. + - `services.caddy` now supports setting `httpPort` and `httpsPort` and opening them in the firewall via `openFirewall`. - The latest available version of Nextcloud is v33 (available as `pkgs.nextcloud33`). The installation logic is as follows: diff --git a/nixos/modules/services/networking/firefox-syncserver.md b/nixos/modules/services/networking/firefox-syncserver.md index 991e97f799d6e..3bc45cfa5640b 100644 --- a/nixos/modules/services/networking/firefox-syncserver.md +++ b/nixos/modules/services/networking/firefox-syncserver.md @@ -32,6 +32,29 @@ This configuration should never be used in production. It is not encrypted and stores its secrets in a world-readable location. ::: +## Database backends {#module-services-firefox-syncserver-database} + +The sync server supports MySQL/MariaDB (the default) and PostgreSQL as database +backends. Set `database.type` to choose the backend: + +```nix +{ + services.firefox-syncserver = { + enable = true; + database.type = "postgresql"; + secrets = "/run/secrets/firefox-syncserver"; + singleNode = { + enable = true; + hostname = "localhost"; + url = "http://localhost:5000"; + }; + }; +} +``` + +When `database.createLocally` is `true` (the default), the module will +automatically enable and configure the corresponding database service. + ## More detailed setup {#module-services-firefox-syncserver-configuration} The `firefox-syncserver` service provides a number of options to make setting up diff --git a/nixos/modules/services/networking/firefox-syncserver.nix b/nixos/modules/services/networking/firefox-syncserver.nix index 6a50e49fc0962..397bcc86161ea 100644 --- a/nixos/modules/services/networking/firefox-syncserver.nix +++ b/nixos/modules/services/networking/firefox-syncserver.nix @@ -13,7 +13,27 @@ let defaultUser = "firefox-syncserver"; dbIsLocal = cfg.database.host == "localhost"; - dbURL = "mysql://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}${lib.optionalString dbIsLocal "?socket=/run/mysqld/mysqld.sock"}"; + dbIsMySQL = cfg.database.type == "mysql"; + dbIsPostgreSQL = cfg.database.type == "postgresql"; + + dbURL = + if dbIsMySQL then + "mysql://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}${lib.optionalString dbIsLocal "?socket=/run/mysqld/mysqld.sock"}" + else + "postgres://${cfg.database.user}@${cfg.database.host}/${cfg.database.name}${lib.optionalString dbIsLocal "?host=/run/postgresql"}"; + + # postgresql.target waits for postgresql-setup.service (which runs + # ensureDatabases / ensureUsers) to complete, avoiding race conditions + # where the syncserver starts before its database and role exist. + dbService = if dbIsMySQL then "mysql.service" else "postgresql.target"; + + syncserver = + if cfg.package != null then + cfg.package + else if dbIsMySQL then + pkgs.syncstorage-rs-mysql + else + pkgs.syncstorage-rs-pgsql; format = pkgs.formats.toml { }; settings = { @@ -22,7 +42,7 @@ let database_url = dbURL; }; tokenserver = { - node_type = "mysql"; + node_type = if dbIsMySQL then "mysql" else "postgres"; database_url = dbURL; fxa_email_domain = "api.accounts.firefox.com"; fxa_oauth_server_url = "https://oauth.accounts.firefox.com/v1"; @@ -41,44 +61,75 @@ let }; }; configFile = format.generate "syncstorage.toml" (lib.recursiveUpdate settings cfg.settings); - setupScript = pkgs.writeShellScript "firefox-syncserver-setup" '' - set -euo pipefail - shopt -s inherit_errexit - schema_configured() { - mysql ${cfg.database.name} -Ne 'SHOW TABLES' | grep -q services - } - - update_config() { - mysql ${cfg.database.name} <<"EOF" - BEGIN; - - INSERT INTO `services` (`id`, `service`, `pattern`) - VALUES (1, 'sync-1.5', '{node}/1.5/{uid}') - ON DUPLICATE KEY UPDATE service='sync-1.5', pattern='{node}/1.5/{uid}'; - INSERT INTO `nodes` (`id`, `service`, `node`, `available`, `current_load`, - `capacity`, `downed`, `backoff`) - VALUES (1, 1, '${cfg.singleNode.url}', ${toString cfg.singleNode.capacity}, - 0, ${toString cfg.singleNode.capacity}, 0, 0) - ON DUPLICATE KEY UPDATE node = '${cfg.singleNode.url}', capacity=${toString cfg.singleNode.capacity}; - - COMMIT; - EOF - } - - - for (( try = 0; try < 60; try++ )); do - if ! schema_configured; then - sleep 2 - else - update_config - exit 0 - fi - done - - echo "Single-node setup failed" - exit 1 - ''; + setupScript = + let + dbSpecific = + if dbIsMySQL then + { + listTables = "mysql ${cfg.database.name} -Ne 'SHOW TABLES'"; + execSql = "mysql ${cfg.database.name}"; + upsertSql = '' + BEGIN; + + INSERT INTO `services` (`id`, `service`, `pattern`) + VALUES (1, 'sync-1.5', '{node}/1.5/{uid}') + ON DUPLICATE KEY UPDATE service='sync-1.5', pattern='{node}/1.5/{uid}'; + INSERT INTO `nodes` (`id`, `service`, `node`, `available`, `current_load`, + `capacity`, `downed`, `backoff`) + VALUES (1, 1, '${cfg.singleNode.url}', ${toString cfg.singleNode.capacity}, + 0, ${toString cfg.singleNode.capacity}, 0, 0) + ON DUPLICATE KEY UPDATE node = '${cfg.singleNode.url}', capacity=${toString cfg.singleNode.capacity}; + + COMMIT; + ''; + } + else + { + listTables = "psql -d ${cfg.database.name} -tAc \"SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = 'services')\""; + execSql = "psql -d ${cfg.database.name}"; + upsertSql = '' + BEGIN; + + INSERT INTO services (id, service, pattern) + VALUES (1, 'sync-1.5', '{node}/1.5/{uid}') + ON CONFLICT (id) DO UPDATE SET service = 'sync-1.5', pattern = '{node}/1.5/{uid}'; + INSERT INTO nodes (id, service, node, available, current_load, + capacity, downed, backoff) + VALUES (1, 1, '${cfg.singleNode.url}', ${toString cfg.singleNode.capacity}, + 0, ${toString cfg.singleNode.capacity}, 0, 0) + ON CONFLICT (id) DO UPDATE SET node = '${cfg.singleNode.url}', capacity = ${toString cfg.singleNode.capacity}; + + COMMIT; + ''; + }; + in + pkgs.writeShellScript "firefox-syncserver-setup" '' + set -euo pipefail + shopt -s inherit_errexit + + schema_configured() { + ${dbSpecific.listTables} | grep -q services + } + + update_config() { + ${dbSpecific.execSql} <<'EOF' + ${dbSpecific.upsertSql} + EOF + } + + for (( try = 0; try < 60; try++ )); do + if ! schema_configured; then + sleep 2 + else + update_config + exit 0 + fi + done + + echo "Single-node setup failed" + exit 1 + ''; in { @@ -88,25 +139,39 @@ in the Firefox Sync storage service. Out of the box this will not be very useful unless you also configure at least - one service and one nodes by inserting them into the mysql database manually, e.g. - by running - - ``` - INSERT INTO `services` (`id`, `service`, `pattern`) VALUES ('1', 'sync-1.5', '{node}/1.5/{uid}'); - INSERT INTO `nodes` (`id`, `service`, `node`, `available`, `current_load`, - `capacity`, `downed`, `backoff`) - VALUES ('1', '1', 'https://mydomain.tld', '1', '0', '10', '0', '0'); - ``` + one service and one nodes by inserting them into the database manually, e.g. + by running the equivalent SQL for your database backend. {option}`${opt.singleNode.enable}` does this automatically when enabled ''; - package = lib.mkPackageOption pkgs "syncstorage-rs" { }; + package = lib.mkOption { + type = lib.types.nullOr lib.types.package; + default = null; + defaultText = lib.literalExpression '' + if config.${opt.database.type} == "mysql" then + pkgs.syncstorage-rs-mysql + else + pkgs.syncstorage-rs-pgsql + ''; + description = '' + Syncstorage server package to use. When `null`, the package is + selected automatically based on {option}`${opt.database.type}`. + ''; + }; + + database.type = lib.mkOption { + type = lib.types.enum [ + "mysql" + "postgresql" + ]; + default = "mysql"; + description = '' + Which database backend to use for storage. + ''; + }; database.name = lib.mkOption { - # the mysql module does not allow `-quoting without resorting to shell - # escaping, so we restrict db names for forward compaitiblity should this - # behavior ever change. type = lib.types.strMatching "[a-z_][a-z0-9_]*"; default = defaultDatabase; description = '' @@ -117,9 +182,15 @@ in database.user = lib.mkOption { type = lib.types.str; - default = defaultUser; + default = if dbIsPostgreSQL then defaultDatabase else defaultUser; + defaultText = lib.literalExpression '' + if database.type == "postgresql" then "${defaultDatabase}" else "${defaultUser}" + ''; description = '' - Username for database connections. + Username for database connections. When using PostgreSQL with + `createLocally`, this defaults to the database name so that + `ensureDBOwnership` works (it requires user and database names + to match). ''; }; @@ -137,7 +208,8 @@ in default = true; description = '' Whether to create database and user on the local machine if they do not exist. - This includes enabling unix domain socket authentication for the configured user. + This includes enabling the configured database service and setting up + authentication for the configured user. ''; }; @@ -237,7 +309,7 @@ in }; config = lib.mkIf cfg.enable { - services.mysql = lib.mkIf cfg.database.createLocally { + services.mysql = lib.mkIf (cfg.database.createLocally && dbIsMySQL) { enable = true; ensureDatabases = [ cfg.database.name ]; ensureUsers = [ @@ -250,16 +322,27 @@ in ]; }; + services.postgresql = lib.mkIf (cfg.database.createLocally && dbIsPostgreSQL) { + enable = true; + ensureDatabases = [ cfg.database.name ]; + ensureUsers = [ + { + name = cfg.database.user; + ensureDBOwnership = true; + } + ]; + }; + systemd.services.firefox-syncserver = { wantedBy = [ "multi-user.target" ]; - requires = lib.mkIf dbIsLocal [ "mysql.service" ]; - after = lib.mkIf dbIsLocal [ "mysql.service" ]; + requires = lib.mkIf dbIsLocal [ dbService ]; + after = lib.mkIf dbIsLocal [ dbService ]; restartTriggers = lib.optional cfg.singleNode.enable setupScript; environment.RUST_LOG = cfg.logLevel; serviceConfig = { - User = defaultUser; - Group = defaultUser; - ExecStart = "${cfg.package}/bin/syncserver --config ${configFile}"; + User = cfg.database.user; + Group = cfg.database.user; + ExecStart = "${syncserver}/bin/syncserver --config ${configFile}"; EnvironmentFile = lib.mkIf (cfg.secrets != null) "${cfg.secrets}"; # hardening @@ -303,10 +386,19 @@ in systemd.services.firefox-syncserver-setup = lib.mkIf cfg.singleNode.enable { wantedBy = [ "firefox-syncserver.service" ]; - requires = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal "mysql.service"; - after = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal "mysql.service"; - path = [ config.services.mysql.package ]; - serviceConfig.ExecStart = [ "${setupScript}" ]; + requires = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal dbService; + after = [ "firefox-syncserver.service" ] ++ lib.optional dbIsLocal dbService; + path = + if dbIsMySQL then [ config.services.mysql.package ] else [ config.services.postgresql.package ]; + serviceConfig = { + ExecStart = [ "${setupScript}" ]; + } + // lib.optionalAttrs dbIsPostgreSQL { + # PostgreSQL peer authentication requires the system user to match the + # database user. Run as the superuser so we can access all databases. + User = "postgres"; + Group = "postgres"; + }; }; services.nginx.virtualHosts = lib.mkIf cfg.singleNode.enableNginx { diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index afad90d187052..07ca9c2613950 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -570,7 +570,7 @@ in imports = [ ./firefox.nix ]; _module.args.firefoxPackage = pkgs.firefox-esr-140; }; - firefox-syncserver = runTest ./firefox-syncserver.nix; + firefox-syncserver = discoverTests (import ./firefox-syncserver.nix); firefox_decrypt = runTest ./firefox_decrypt.nix; firefoxpwa = runTest ./firefoxpwa.nix; firejail = runTest ./firejail.nix; diff --git a/nixos/tests/firefox-syncserver.nix b/nixos/tests/firefox-syncserver.nix index 04b6fdc887115..0998aa8b836e6 100644 --- a/nixos/tests/firefox-syncserver.nix +++ b/nixos/tests/firefox-syncserver.nix @@ -1,32 +1,54 @@ -{ - pkgs, - ... -}: +let + makeFirefoxSyncserverTest = + name: + { + backend ? name, + }: + import ./make-test-python.nix ( + { lib, pkgs, ... }: + { + name = "firefox-syncserver-${name}"; -{ - name = "firefox-syncserver"; - nodes.machine = { - services.mysql = { - enable = true; - package = pkgs.mariadb; - }; + nodes.machine = + { pkgs, ... }: + lib.mkMerge [ + { + mysql = { + services.mysql = { + enable = true; + package = pkgs.mariadb; + }; + }; + postgresql = { }; + } + .${backend} - services.firefox-syncserver = { - enable = true; - secrets = pkgs.writeText "secret" "this-is-a-test"; - singleNode = { - enable = true; - hostname = "firefox-syncserver.local"; - capacity = 1; - }; - }; - }; + { + services.firefox-syncserver = { + enable = true; + database.type = backend; + secrets = pkgs.writeText "sync-secrets" '' + SYNC_MASTER_SECRET=a-]test-secret-that-is-not-real + ''; + singleNode = { + enable = true; + hostname = "firefox-syncserver.local"; + capacity = 1; + }; + }; + } + ]; - testScript = '' - machine.wait_for_unit("firefox-syncserver.service") - machine.wait_for_open_port(5000) - - machine.wait_until_succeeds("curl --fail http://127.0.0.1:5000") - - ''; + testScript = '' + machine.wait_for_unit("multi-user.target") + machine.wait_until_succeeds("systemctl is-active firefox-syncserver.service", timeout=120) + machine.wait_for_open_port(5000) + machine.wait_until_succeeds("curl --fail http://127.0.0.1:5000") + ''; + } + ); +in +builtins.mapAttrs (k: v: makeFirefoxSyncserverTest k v) { + mysql = { }; + postgresql = { }; } diff --git a/pkgs/by-name/sy/syncstorage-rs/package.nix b/pkgs/by-name/sy/syncstorage-rs/package.nix index d177af7c8a82a..422f664977711 100644 --- a/pkgs/by-name/sy/syncstorage-rs/package.nix +++ b/pkgs/by-name/sy/syncstorage-rs/package.nix @@ -1,14 +1,18 @@ { fetchFromGitHub, + fetchurl, rustPlatform, pkg-config, python3, cmake, libmysqlclient, + libpq, + openssl, makeBinaryWrapper, lib, nix-update-script, nixosTests, + dbBackend ? "mysql", }: let @@ -19,17 +23,25 @@ let p.tokenlib p.cryptography ]); + # utoipa-swagger-ui downloads Swagger UI assets at build time. + # Prefetch the archive for sandboxed builds. + swaggerUi = fetchurl { + url = "https://github.com/swagger-api/swagger-ui/archive/refs/tags/v5.17.14.zip"; + hash = "sha256-SBJE0IEgl7Efuu73n3HZQrFxYX+cn5UU5jrL4T5xzNw="; + }; in rustPlatform.buildRustPackage (finalAttrs: { pname = "syncstorage-rs"; - version = "0.21.1-unstable-2026-01-26"; + version = "0.22.2"; + + __structuredAttrs = true; src = fetchFromGitHub { owner = "mozilla-services"; repo = "syncstorage-rs"; - rev = "11659d98f9c69948a0aab353437ce2036c388711"; - hash = "sha256-G37QvxTNh/C3gmKG0UYHI6QBr0F+KLGRNI/Sx33uOsc="; + rev = "refs/tags/${finalAttrs.version}"; + hash = "sha256-hEDa9hk00QvMY86zrtTq3+UOmbNehDb7Ya8St9u6IuA="; }; nativeBuildInputs = [ @@ -39,23 +51,47 @@ rustPlatform.buildRustPackage (finalAttrs: { python3 ]; - buildInputs = [ - libmysqlclient - ]; + buildInputs = + lib.optional (dbBackend == "mysql") libmysqlclient + ++ lib.optionals (dbBackend == "postgresql") [ + libpq + openssl + ]; + + buildNoDefaultFeatures = true; + # The syncserver "postgres" feature only enables syncstorage-db/postgres. + # tokenserver-db/postgres must be enabled separately so the tokenserver + # can also connect to PostgreSQL (it dispatches on the URL scheme at runtime). + buildFeatures = + let + cargoFeature = if dbBackend == "postgresql" then "postgres" else dbBackend; + in + [ + cargoFeature + "tokenserver-db/${cargoFeature}" + "py_verifier" + ]; + + env = { + SWAGGER_UI_DOWNLOAD_URL = "file://${swaggerUi}"; + }; preFixup = '' wrapProgram $out/bin/syncserver \ --prefix PATH : ${lib.makeBinPath [ pyFxADeps ]} ''; - cargoHash = "sha256-9Dcf5mDyK/XjsKTlCPXTHoBkIq+FFPDg1zfK24Y9nHQ="; + cargoHash = "sha256-lTjvRTenmxYAYS5HB32x19DLkdd09jeWOhUbzt7TQ4Y="; # almost all tests need a DB to test against doCheck = false; passthru.updateScript = nix-update-script { }; - passthru.tests = { inherit (nixosTests) firefox-syncserver; }; + passthru.tests = { + firefox-syncserver = + nixosTests.firefox-syncserver.${if dbBackend == "postgresql" then "postgresql" else "mysql"}; + }; meta = { description = "Mozilla Sync Storage built with Rust"; diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 2780fed25c60e..b1d3107782a69 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -10336,6 +10336,13 @@ with pkgs; waylandSupport = true; }; + syncstorage-rs-mysql = callPackage ../by-name/sy/syncstorage-rs/package.nix { + dbBackend = "mysql"; + }; + syncstorage-rs-pgsql = callPackage ../by-name/sy/syncstorage-rs/package.nix { + dbBackend = "postgresql"; + }; + inherit (callPackages ../applications/networking/syncthing { inherit (darwin) autoSignDarwinBinariesHook;