From adcb59ef1cb09d544c8e5f4b1958a1fd589b914d Mon Sep 17 00:00:00 2001 From: Julien Moutinho Date: Wed, 24 Dec 2025 04:48:50 +0100 Subject: [PATCH 1/3] maint/update(bonfire): build `deps.nix` using `--refresh` --- pkgs/by-name/bonfire/update.nix | 1 + 1 file changed, 1 insertion(+) diff --git a/pkgs/by-name/bonfire/update.nix b/pkgs/by-name/bonfire/update.nix index 416c88395..699cbe97a 100644 --- a/pkgs/by-name/bonfire/update.nix +++ b/pkgs/by-name/bonfire/update.nix @@ -47,6 +47,7 @@ in --option sandbox relaxed \ --no-link --print-out-paths \ --repair \ + --refresh \ -f . \ bonfire.${FLAVOUR}.passthru.update.package ) From 1c041c07dc755d115d937fbcf7411ed680c11e9b Mon Sep 17 00:00:00 2001 From: Julien Moutinho Date: Fri, 14 Nov 2025 01:25:11 +0100 Subject: [PATCH 2/3] projects(bonfire): init service --- profiles/nixos/nginx/reverse-proxy.nix | 89 +++ projects/Bonfire/default.nix | 50 ++ projects/Bonfire/demo/module-demo.nix | 20 + projects/Bonfire/demo/module.nix | 5 + .../services/bonfire/examples/basic.nix | 25 + projects/Bonfire/services/bonfire/module.nix | 518 ++++++++++++++++++ .../Bonfire/services/bonfire/tests/basic.nix | 102 ++++ .../services/bonfire/tests/selenium.nix | 106 ++++ 8 files changed, 915 insertions(+) create mode 100644 profiles/nixos/nginx/reverse-proxy.nix create mode 100644 projects/Bonfire/default.nix create mode 100644 projects/Bonfire/demo/module-demo.nix create mode 100644 projects/Bonfire/demo/module.nix create mode 100644 projects/Bonfire/services/bonfire/examples/basic.nix create mode 100644 projects/Bonfire/services/bonfire/module.nix create mode 100644 projects/Bonfire/services/bonfire/tests/basic.nix create mode 100644 projects/Bonfire/services/bonfire/tests/selenium.nix diff --git a/profiles/nixos/nginx/reverse-proxy.nix b/profiles/nixos/nginx/reverse-proxy.nix new file mode 100644 index 000000000..851af10fd --- /dev/null +++ b/profiles/nixos/nginx/reverse-proxy.nix @@ -0,0 +1,89 @@ +{ + service, + location ? "/", + proxyPass ? "http://unix:/run/${service}/socket", + recommendedProxySettings ? true, + proxyWebsockets ? false, + group ? service, + virtualHost ? { }, +}: + +{ + lib, + config, + options, + modulesPath, + ... +}: + +let + cfg = config.services.${service}; +in +{ + # Explanation: https://nixos.org/manual/nixos/unstable/#modular-services + _class = "nixos"; + + options = { + services.${service} = { + nginx = { + enable = lib.mkEnableOption "an Nginx reverse-proxy to ${service}"; + virtualHost = lib.mkOption { + description = '' + With this option, you can customize an nginx virtual host which already has sensible defaults for `${service}`. + Set to `{}` if you do not need any customization to the virtual host. + If enabled, then by default, the {option}`serverName` is + `${service}.''${config.networking.domain}`, + TLS is active, and certificates are acquired via ACME. + If this is set to null (the default), no nginx virtual host will be configured. + ''; + default = { }; + example = lib.literalExpression '' + { + enableACME = false; + useACMEHost = config.networking.domain; + } + ''; + type = lib.types.submodule ( + lib.recursiveUpdate + (import (modulesPath + "/services/web-servers/nginx/vhost-options.nix") { + inherit config lib; + }) + { + options.serverName = { + default = "${service}.${config.networking.domain}"; + defaultText = "${service}.\${config.networking.domain}"; + }; + } + ); + }; + }; + }; + }; + + config = lib.mkMerge [ + (lib.mkIf cfg.nginx.enable { + services.nginx = { + enable = true; + virtualHosts.${cfg.nginx.virtualHost.serverName} = lib.mkMerge [ + virtualHost + cfg.nginx.virtualHost + { + forceSSL = lib.mkDefault true; + enableACME = lib.mkDefault true; + locations.${location} = { + proxyPass = lib.mkDefault proxyPass; + recommendedProxySettings = lib.mkDefault recommendedProxySettings; + proxyWebsockets = lib.mkDefault proxyWebsockets; + }; + } + ]; + }; + }) + (lib.optionalAttrs (options ? systemd) { + systemd.services.nginx.serviceConfig.SupplementaryGroups = [ + group + ]; + }) + ]; + +} diff --git a/projects/Bonfire/default.nix b/projects/Bonfire/default.nix new file mode 100644 index 000000000..43aecc737 --- /dev/null +++ b/projects/Bonfire/default.nix @@ -0,0 +1,50 @@ +{ + lib, + pkgs, + sources, + ... +}@args: + +{ + metadata = { + summary = "An open-source framework for building federated digital spaces where people can gather, interact, and form communities online"; + subgrants = { + Commons = [ ]; + Core = [ ]; + Entrust = [ + "Bonfire-FederatedGroups" + "Bonfire-Framework" + ]; + Review = [ "Bonfire" ]; + }; + links = { + homepage = { + text = "Home page"; + url = "https://bonfirenetworks.org"; + }; + src = { + text = "Source code (only the top-level repository)"; + url = "https://github.com/bonfire-networks/bonfire-app"; + }; + }; + }; + + nixos.modules.services = { + bonfire = { + name = "service name"; + module = ./services/bonfire/module.nix; + examples."Enable bonfire" = { + module = ./services/bonfire/examples/basic.nix; + description = '' + Usage instructions + + 1. Run `nix -L run -f . hydrated-projects.Bonfire.nixos.tests.basic.driverInteractive` + 2. Open your browser to + 3. Create an account. + ''; + tests.basic.module = import ./services/bonfire/tests/basic.nix args; + }; + }; + }; + +} diff --git a/projects/Bonfire/demo/module-demo.nix b/projects/Bonfire/demo/module-demo.nix new file mode 100644 index 000000000..fe8a6105e --- /dev/null +++ b/projects/Bonfire/demo/module-demo.nix @@ -0,0 +1,20 @@ +{ + lib, + config, + ... +}: +let + cfg = config.programs.bonfire; +in +{ + config = lib.mkIf cfg.enable { + demo-shell = { + programs = { + bonfire = cfg.package; + }; + env = { + PROGRAM_PORT = toString cfg.port; + }; + }; + }; +} diff --git a/projects/Bonfire/demo/module.nix b/projects/Bonfire/demo/module.nix new file mode 100644 index 000000000..e275b9566 --- /dev/null +++ b/projects/Bonfire/demo/module.nix @@ -0,0 +1,5 @@ +{ ... }: + +{ + programs.bonfire.enable = true; +} diff --git a/projects/Bonfire/services/bonfire/examples/basic.nix b/projects/Bonfire/services/bonfire/examples/basic.nix new file mode 100644 index 000000000..c352dc4c0 --- /dev/null +++ b/projects/Bonfire/services/bonfire/examples/basic.nix @@ -0,0 +1,25 @@ +{ pkgs, ... }: +{ + networking.domain = "localdomain"; + services.bonfire = { + enable = true; + settings = { + HOSTNAME = "localhost"; + PUBLIC_PORT = 80; + }; + postgresql.enable = true; + meilisearch.enable = true; + nginx = { + enable = true; + virtualHost = { + serverAliases = [ + "localhost" + "localhost.localdomain" + ]; + forceSSL = false; + enableACME = false; + }; + }; + }; + services.meilisearch.masterKeyFile = pkgs.writeText "meilisearch.masterKeyFile" "675b2c63f569d0bb3f872517b903fa9ea3ddce19d5766c80a8"; +} diff --git a/projects/Bonfire/services/bonfire/module.nix b/projects/Bonfire/services/bonfire/module.nix new file mode 100644 index 000000000..2ddb164c9 --- /dev/null +++ b/projects/Bonfire/services/bonfire/module.nix @@ -0,0 +1,518 @@ +{ + config, + pkgs, + lib, + ... +}: +let + service = "bonfire"; + cfg = config.services.${service}; + stateDir = "/var/lib/${service}"; + + # Warning(-security/confidentiality): + # even though secrets are read from files (encrypted or outside the Nix store), + # they end up in environment variables. + # + # Issue: https://github.com/bonfire-networks/bonfire-app/issues/1663 + # + # ToDo(+security/confidentiality): move entries from `secretsStillUnsafe` to `secrets` + # whenever they can remain in a file instead of going into an env-var. + secretsStillUnsafe = [ + "ENCRYPTION_SALT" + "MEILI_MASTER_KEY" + "POSTGRES_PASSWORD" + "RELEASE_COOKIE" + "SECRET_KEY_BASE" + "SIGNING_SALT" + ]; + secrets = [ ]; + + elixirFormat = pkgs.formats.elixirConf { elixir = cfg.package.elixir; }; + inherit (elixirFormat.lib) mkAtom mkRaw mkTuple; + +in +{ + imports = [ + (lib.modules.importApply ../../../../profiles/nixos/nginx/reverse-proxy.nix { + inherit service; + proxyPass = "http://localhost:${toString cfg.settings.SERVER_PORT}"; + proxyWebsockets = true; + }) + ]; + + options.services.bonfire = { + enable = lib.mkEnableOption "bonfire"; + openFirewall = lib.mkEnableOption '' + opening the firewall for Bonfire's PUBLIC_PORT. + This is only necessary if you do not use a reverse-proxy + ''; + package = lib.mkPackageOption pkgs [ "bonfire" "social" ] { }; + settings = lib.mkOption { + description = '' + Configuration for Bonfire, will be passed as environment variables. + See . + ''; + default = { }; + type = lib.types.submodule { + freeformType = + with lib.types; + attrsOf (oneOf [ + bool + int + path + port + str + ]); + + options = { + DB_MIGRATE_INDEXES_CONCURRENTLY = lib.mkEnableOption '' + disable changes to the database schema when upgrading Bonfire + + Bonfire initialization fails hard with concurrent indexing, + yet it may be enabled after initial migrations were run + if you feel lucky. + ''; + DB_QUERIES_LOG_LEVEL = lib.mkOption { + # Source: https://www.erlang.org/docs/26/apps/kernel/logger_chapter#log_level + type = lib.types.enum [ + "emergency" + "alert" + "critical" + "error" + "warning" + "notice" + "info" + "debug" + ]; + description = "The log level."; + default = "warning"; + }; + DISABLE_DB_AUTOMIGRATION = lib.mkEnableOption "disable changes to the database schema when upgrading Bonfire"; + ECTO_IPV6 = + lib.mkEnableOption '' + IPv6 when connecting to the PostgreSQL database. + + Do not enable it when connecting through a Unix socket, + it would make it fail + '' + // { + default = config.networking.enableIPv6 && !(lib.types.path.check cfg.settings.POSTGRES_HOST); + defaultText = lib.literalExpression "config.networking.enableIPv6 && !(lib.types.path.check config.services.bonfire.settings.POSTGRES_HOST)"; + }; + ENCRYPTION_SALT = lib.mkOption { + type = lib.types.str; + description = '' + The systemd credential name of the encryption salt, + resolved from systemd credential stores + as documented at + ''; + default = "${service}.ENCRYPTION_SALT"; + }; + FEDERATE = + lib.mkEnableOption '' + federate + '' + // { + default = false; + }; + HOSTNAME = lib.mkOption { + type = lib.types.str; + default = "${service}.${config.networking.domain}"; + defaultText = lib.literalExpression "bonfire-\${config.networking.domain}"; + description = '' + Hostname your visitors will use to access bonfire. + ''; + }; + LANG = lib.mkOption { + type = lib.types.str; + default = "en_US.UTF-8"; + description = "Default language and locale."; + }; + LANGUAGE = lib.mkOption { + type = lib.types.str; + default = "en_US.UTF-8"; + description = "Default language and locale."; + }; + MAIL_BACKEND = lib.mkOption { + type = lib.types.enum [ + "smtp" + "mailgun" + "none" + ]; + description = "The mail backend to use."; + default = "none"; + }; + MEILI_MASTER_KEY = lib.mkOption { + type = with lib.types; nullOr str; + description = '' + The systemd credential name of the Meilisearch master key, + resolved from systemd credential stores + as documented at + ''; + default = if cfg.meilisearch.enable then "${service}.MEILI_MASTER_KEY" else null; + defaultText = lib.literalExpression '' + if config.services.meilisearch.enable then "${service}.MEILI_MASTER_KEY" else null + ''; + }; + PLUG_SERVER = lib.mkOption { + type = lib.types.enum [ + "bandit" + "cowboy" + ]; + description = "Webserver to use."; + default = "cowboy"; + }; + POSTGRES_HOST = lib.mkOption { + type = lib.types.str; + description = '' + Hostname or Unix socket **directory** to connect to the PostgreSQL database. + ''; + default = "/run/postgresql"; + }; + POSTGRES_DB = lib.mkOption { + type = lib.types.str; + description = "name of the PostgreSQL database"; + default = config.users.users.${service}.name; + defaultText = lib.literalExpression "config.users.users.${service}.name"; + }; + POSTGRES_PASSWORD = lib.mkOption { + type = with lib.types; nullOr str; + description = '' + The systemd credential name of the password to connect to the PostgreSQL database, + resolved from systemd credential stores + as documented at + ''; + default = "${service}.POSTGRES_PASSWORD"; + }; + POSTGRES_USER = lib.mkOption { + type = lib.types.str; + description = "role name to connect to the PostgreSQL database"; + default = config.users.users.${service}.name; + defaultText = lib.literalExpression "config.users.users.${service}.name"; + }; + PUBLIC_PORT = lib.mkOption { + type = lib.types.port; + default = 4000; + description = '' + Port your visitors will use to access bonfire + (typically 80 or 443 if using a reverse-proxy). + ''; + }; + RELEASE_COOKIE = lib.mkOption { + type = with lib.types; nullOr str; + description = '' + The systemd credential name of the Erlang Distribution cookie, + resolved from systemd credential stores + as documented at + + It's recommend to use a long and randomly generated string such as: + `head -c 40 /dev/random | base32`. + It's also recommended to only use alphanumeric + characters and underscores. + + All Bonfire components in your cluster must use the same value. + + If this is `null`, a shared value will automatically be generated + on startup and used for all components on this machine. + You do not need to set this except when you spread your cluster + over multiple hosts. + ''; + default = "${service}.RELEASE_COOKIE"; + }; + SEARCH_MEILI_INSTANCE = lib.mkOption { + type = with lib.types; nullOr str; + description = '' + Hostname and port of Meilisearch search index. + ''; + default = null; + }; + SECRET_KEY_BASE = lib.mkOption { + type = with lib.types; nullOr str; + description = '' + The systemd credential name of the key to sign/encrypt cookies and other secrets, + resolved from systemd credential stores + as documented at + + It should be a unique base64 encoded secret. + All Bonfire components in your cluster must use the same value. + + If this is `null`, a shared value will automatically be generated + on startup and used for all components on this machine. + You do not need to set this except when you spread your cluster + over multiple hosts. + ''; + default = "${service}.SECRET_KEY_BASE"; + }; + SIGNING_SALT = lib.mkOption { + type = lib.types.str; + description = '' + The systemd credential name of the signing salt, + resolved from systemd credential stores + as documented at + ''; + default = "${service}.SIGNING_SALT"; + }; + SERVER_PORT = lib.mkOption { + type = lib.types.port; + default = 4000; + description = "Bonfire port."; + }; + }; + }; + }; + + elixirSettings = lib.mkOption { + description = '' + Runtime Elixir configuration for Bonfire. + ''; + default = { }; + type = lib.types.submodule { + freeformType = elixirFormat.type; + options = { + ":bonfire" = { + "Bonfire.Web.Endpoint" = { + http = { + ip = lib.mkOption { + description = "Listening IP address or Unix socket."; + default = mkTuple [ + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + ]; + defaultText = lib.literalExpression '' + (pkgs.formats.elixirConf { }).lib.mkTuple [0 0 0 0 0 0 0 0] + ''; + example = lib.literalExpression '' + (pkgs.formats.elixirConf { }).lib.mkTuple [0 0 0 0 0 0 0 0] + ''; + }; + }; + }; + }; + ":tzdata" = { + ":data_dir" = lib.mkOption { + internal = true; + description = '' + :tzdata needs a writable directory to auto-update + its TimeZone data periodically. + ''; + default = mkRaw ''"${stateDir}/tzdata"''; + }; + }; + }; + }; + }; + + meilisearch = { + enable = lib.mkEnableOption "running a local Meilisearch search engine"; + }; + + postgresql = { + enable = lib.mkEnableOption "running a local PostgreSQL database"; + }; + }; + + config = lib.mkIf cfg.enable ( + lib.mkMerge [ + { + systemd.services.bonfire = { + description = "Bonfire"; + after = [ + "network.target" + "epmd.socket" + ]; + wantedBy = [ "multi-user.target" ]; + environment = + let + envs = + cfg.settings + // { + DB_MIGRATE_INDEXES_CONCURRENTLY = + if cfg.settings.DB_MIGRATE_INDEXES_CONCURRENTLY then true else "false"; + # Explanation: behaves like true even when set to false (default), because + # ecto_sparkles/lib/schema_migration/auto_migrator.ex + # checks only for the existence of this envvar, not its content. + DISABLE_DB_AUTOMIGRATION = if cfg.settings.DISABLE_DB_AUTOMIGRATION then true else null; + } + // { + # Explanation: bonfire_common/lib/runtime_config.ex tests only for the string "false". + # HowTo(maint/analyze): cat $(nix -L build --no-link --print-out-paths -f . \ + # projects.Bonfire.nixos.tests.basic.nodes.machine.systemd.services.bonfire.environment.BONFIRE_RUNTIME_CONFIG) + BONFIRE_RUNTIME_CONFIG = elixirFormat.generate "runtime.exs" cfg.elixirSettings; + } + // lib.optionalAttrs (lib.types.path.check cfg.settings.POSTGRES_HOST) { + # Explanation: using a Unix socket directly doesn't work without setting PGHOST too. + # Issue: https://github.com/elixir-ecto/postgrex/issues/415#issuecomment-2622198966 + PGHOST = cfg.settings.POSTGRES_HOST; + }; + in + lib.pipe envs [ + (lib.filterAttrs (name: value: !lib.elem name (secretsStillUnsafe ++ secrets) && value != null)) + (lib.mapAttrs ( + name: value: + # Explanation: convert to string only simple types + # otherwise keep as is to preserve any string context. + if lib.isBool value || lib.isInt value then toString value else value + )) + ] + // lib.genAttrs (lib.filter (secret: cfg.settings.${secret} != null) secrets) ( + secret: "%d/${cfg.settings.${secret}}" + ); + + serviceConfig = { + User = config.users.users.${service}.name; + Group = config.users.groups.${service}.name; + ExecStart = lib.getExe ( + pkgs.writeShellApplication { + name = "${service}-ExecStart"; + text = '' + set -x + '' + + + # Description: load secretsStillUnsafe into env-vars + # from bonfire.* credentials. + lib.concatMapStringsSep "\n" ( + secret: + lib.optionalString (cfg.settings.${secret} != null) '' + ${secret}=''$(systemd-creds cat ${lib.escapeShellArg cfg.settings.${secret}}) + export ${secret} + '' + ) secretsStillUnsafe + + '' + exec ${lib.getExe cfg.package} start + ''; + } + ); + ExecStop = "${lib.getExe cfg.package} stop"; + RuntimeDirectory = [ service ]; + StateDirectory = [ service ]; + # Explanation: make Bonfire puts data uploaded by its users in ${stateDir}/data/ + WorkingDirectory = stateDir; + Restart = lib.mkDefault "on-failure"; + RestartSec = lib.mkDefault 10; + ImportCredential = lib.concatMap ( + secret: if cfg.settings.${secret} == null then [ ] else [ cfg.settings.${secret} ] + ) (secretsStillUnsafe ++ secrets); + }; + }; + + # Explanation: do not let ${service}.service be in charge of epmd. + services.epmd = { + enable = true; + }; + + users = { + users.${service} = { + description = "Bonfire"; + group = service; + home = stateDir; + isSystemUser = true; + }; + groups.${service} = { + }; + }; + + networking.firewall = lib.mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.settings.PUBLIC_PORT ]; + }; + } + + (lib.mkIf cfg.postgresql.enable { + systemd.services.${service} = { + after = [ "postgresql.target" ]; + requires = [ "postgresql.target" ]; + }; + services.postgresql = { + enable = true; + ensureDatabases = [ cfg.settings.POSTGRES_DB ]; + extensions = lib.mkIf (cfg.package.mixNixDeps ? "geo_postgis") ( + with config.services.postgresql.package.pkgs; [ postgis ] + ); + ensureUsers = [ + { + name = cfg.settings.POSTGRES_USER; + ensureDBOwnership = true; + ensureClauses.login = true; + } + ]; + }; + systemd.services.postgresql-setup = lib.mkIf (!(lib.types.path.check cfg.settings.POSTGRES_HOST)) { + path = [ + config.services.postgresql + pkgs.gnused + pkgs.replace-secret + ]; + postStart = '' + install -m600 ${pkgs.writeText "" '' + ALTER ROLE ${config.users.users.${service}.name} WITH ENCRYPTED PASSWORD '@DB_USER_PASSWORD@'; + ''} /run/${service}/init.sql + replace-secret @DB_USER_PASSWORD@ $CREDENTIALS_DIRECTORY/${service}.POSTGRES_PASSWORD /run/${service}/init.sql + psql -U postgres --file /run/${service}/init.sql + rm /run/${service}/init.sql + ''; + }; + }) + + (lib.mkIf cfg.nginx.enable { + services.${service} = { + settings = { + HOSTNAME = lib.mkDefault cfg.nginx.serverName; + PUBLIC_PORT = lib.mkDefault 443; + }; + elixirSettings = { + /* + FixMe(+optimize +security +co-existence): + Bonfire does not yet support Unix socket + Issue: https://github.com/bonfire-networks/bonfire-app/issues/1698 + + ":bonfire"."Bonfire.Web.Endpoint"."http" = { + "ip" = lib.mkDefault (mkTuple [ + (mkAtom ":local") + "/run/bonfire/socket" + ]); + # Explanation: avoid crash: + # ** (EXIT) shutdown: failed to start child: :ranch_acceptors_sup + # ** (EXIT) :badarg + "port" = lib.mkDefault 0; + "transport_options" = { + # Explanation: config/runtime.exs sets :inet6 which is not compatible with a Unix socket + "socket_opts" = lib.mkDefault [ ]; + # Explanation: let the group access the socket. + post_listen_callback = lib.mkDefault ( + # mkRaw "fn (:local, sock) -> File.chmod!(sock, 0o660) end" + mkRaw ''fn _ -> File.chmod!("/run/bonfire/socket", 0o660) end'' + ); + }; + }; + */ + }; + }; + }) + + (lib.mkIf cfg.meilisearch.enable { + services.meilisearch = { + enable = true; + }; + systemd.services.${service}.serviceConfig = { + LoadCredential = [ "${service}.MEILI_MASTER_KEY:${config.services.meilisearch.masterKeyFile}" ]; + }; + services.${service}.settings = { + SEARCH_MEILI_INSTANCE = "http://${config.services.meilisearch.listenAddress}:${toString config.services.meilisearch.listenPort}"; + }; + }) + ] + ); + + meta = { + maintainers = + lib.teams.ngi.members + ++ (with lib.maintainers; [ + julm + ]); + }; +} diff --git a/projects/Bonfire/services/bonfire/tests/basic.nix b/projects/Bonfire/services/bonfire/tests/basic.nix new file mode 100644 index 000000000..537e7f545 --- /dev/null +++ b/projects/Bonfire/services/bonfire/tests/basic.nix @@ -0,0 +1,102 @@ +{ + sources, + ... +}: + +{ + name = "Bonfire"; + + nodes = { + machine = + { pkgs, lib, ... }: + { + imports = [ + sources.modules.ngipkgs + sources.modules.services.bonfire + sources.examples.Bonfire."Enable bonfire" + ]; + + # Explanation: increased to avoid: + # Kernel panic - not syncing: Out of memory + # as soon as running the initial migration of the PostgreSQL schema. + virtualisation.memorySize = 4096; + + virtualisation.credentials = { + # openssl rand -hex 128 + "bonfire.ENCRYPTION_SALT".text = + "fde9939363a25b2696a7cfd738afcb19f82e2212bca4124d2c70102f3809974c618aeaa279e4daa062b53e07e7d14b4297409a582389a94bac247de13da116d76d6644174d21ad3814ddd7269696997447b8c8fb5f75aa757a8f32148708bb38bf0d66f1dd4a206e9ab3b3818f79dc48303c9375fa68210dbd8567f3a5bcf4f2"; + # openssl rand -hex 25 + "bonfire.POSTGRES_PASSWORD".text = "ced4a928ed2305630f7865a160b26bc6ab690c445529340fcf"; + # openssl rand -hex 40 + "bonfire.RELEASE_COOKIE".text = + "1255749c5082f5c64d6984231a02095f6273875363008a0a6ed2c413bbd7ed66249eeebf8abbae3d"; + # openssl rand -hex 128 + "bonfire.SECRET_KEY_BASE".text = + "0da76ae83b6e2170d3d501ac000dfe96adc820d16cbf54567188f206c9322dcfaf5fac1c5fc6ab742249ff28b69e7b06addc69e02e49290319bb3cc8df0aff920e1f812cf6906ac4711425a7bb7af2f5cf78e03039c8812f04eb2f1ce1ef31a1ff81bc6d4de06ec524171310f6c7fb2ac832f387725842667870081311386b82"; + # openssl rand -hex 128 + "bonfire.SIGNING_SALT".text = + "3278f788f120031c3d2b8dc480fce1dba38b6ce3f16de17df443e24c66a689d75e52516beec260a3f3bf53e8637c7e66591126e25a526dd25e3e26383124656eb9ad94441c31f278852a55cfe8083e8a0fef6b061fa8c34cbe26169a3dd43854c719c2ad269449fe9172193b031b5f76c16813fb7ec0a195289b6eb5ccfaa1ca"; + }; + + environment.systemPackages = [ + # ToDo: check if those are required here + pkgs.firefox-unwrapped + pkgs.geckodriver + (pkgs.callPackage ./selenium.nix { }) + ]; + }; + }; + + interactive = { + # HowTo(maint/debug): + # nix -L run -f . hydrated-projects.Bonfire.nixos.tests.basic.driverInteractive + # python> start_all() + # ssh -o User=root vsock/3 + sshBackdoor.enable = true; + + nodes.machine = + { pkgs, ... }: + { + networking.firewall.allowedTCPPorts = [ 80 ]; + virtualisation.forwardPorts = [ + # HowTo(maint/debug): + # nix -L run -f . hydrated-projects.Bonfire.nixos.tests.basic.driverInteractive + # python> start_all() + # firefox http://localhost:4000 + { + from = "host"; + host.port = 4000; + guest.port = 80; + } + ]; + + }; + }; + + testScript = + { nodes, ... }: + '' + start_all() + + machine.wait_for_unit("postgresql.target") + machine.wait_for_unit("nginx.service") + + with subtest("start bonfire"): + machine.wait_for_unit("bonfire.service") + machine.wait_for_open_port(${toString nodes.machine.config.services.bonfire.settings.PUBLIC_PORT}) + machine.wait_for_open_port(${toString nodes.machine.config.services.bonfire.settings.SERVER_PORT}) + + # ToDo(security): whenever bonfire supports Unix socket + # with subtest("check bonfire socket"): + # socket="/run/bonfire/socket" + # machine.wait_for_file(socket) + # machine.succeed( + # f'[[ "$(stat -c %U {socket})" == "bonfire" ]]', + # f'[[ "$(stat -c %G {socket})" == "bonfire" ]]', + # f'[[ "$(stat -c %a {socket})" == "660" ]]', + # ) + + with subtest("Web interface"): + machine.succeed("PYTHONUNBUFFERED=1 selenium-test") + ''; +} diff --git a/projects/Bonfire/services/bonfire/tests/selenium.nix b/projects/Bonfire/services/bonfire/tests/selenium.nix new file mode 100644 index 000000000..90e4414ff --- /dev/null +++ b/projects/Bonfire/services/bonfire/tests/selenium.nix @@ -0,0 +1,106 @@ +{ + lib, + writers, + python3Packages, + geckodriver, +}: +let + user = "admin"; + domain = "localhost.localdomain"; + url = "http://${domain}"; + email = "${user}@${domain}"; + password = "0123456789"; +in +writers.writePython3Bin "selenium-test" + { + libraries = with python3Packages; [ selenium ]; + flakeIgnore = [ + "E501" # line too long + ]; + } + '' + from selenium import webdriver + from selenium.webdriver.common.by import By + from selenium.webdriver.firefox.options import Options + from selenium.webdriver.support.ui import WebDriverWait + from selenium.webdriver.support import expected_conditions as EC + + + def log(msg): + from sys import stderr + print(f"[*] {msg}", file=stderr) + + + log("Initializing") + + options = Options() + options.add_argument("--headless") + + service = webdriver.FirefoxService(executable_path="${lib.getExe geckodriver}") # noqa: E501 + driver = webdriver.Firefox(options=options, service=service) + + driver.implicitly_wait(30) + driver.set_page_load_timeout(60) + + log("Opening sign up page") + driver.get("${url}/signup") + + + def wait_elem(by, query, timeout=10): + wait = WebDriverWait(driver, timeout) + wait.until(EC.presence_of_element_located((by, query))) + + + def wait_title_contains(title, timeout=10): + wait = WebDriverWait(driver, timeout) + wait.until(EC.title_contains(title)) + + + def find_element(by, query): + return driver.find_element(by, query) + + + def set_value(elem, value): + script = 'arguments[0].value = arguments[1]' + return driver.execute_script(script, elem, value) + + + log("Waiting for the sign up page to load") + + wait_title_contains("Sign up") + wait_elem(By.CSS_SELECTOR, 'input#signup-form_email_0_email_address') + + log("Sign up page loaded!") + + log("Filling out email") + input_login = find_element(By.CSS_SELECTOR, 'input#signup-form_email_0_email_address') + set_value(input_login, "${email}") + + log("Filling out password") + input_password = find_element(By.CSS_SELECTOR, 'input#signup-form_credential_0_password') + set_value(input_password, "${password}") + input_password = find_element(By.CSS_SELECTOR, 'input#signup-form_credential_0_password_confirmation') + set_value(input_password, "${password}") + + log("Submitting credentials for login") + driver.find_element(By.CSS_SELECTOR, 'button[data-role=signup_submit]').click() + + log("Waiting user creation page") + wait_title_contains("Create a new user profile") + input_name = find_element(By.CSS_SELECTOR, 'input#create-user-form_profile_0_name') + set_value(input_name, "${user}") + + input_username = find_element(By.CSS_SELECTOR, 'input#create-user-form_character_0_username') + set_value(input_username, "${user}") + if driver.find_element(By.CSS_SELECTOR, 'input[name=undiscoverable]').is_selected(): + driver.find_element(By.CSS_SELECTOR, 'input[name=undiscoverable]').click() + if driver.find_element(By.CSS_SELECTOR, 'input[name=unindexable]').is_selected(): + driver.find_element(By.CSS_SELECTOR, 'input[name=unindexable]').click() + driver.find_element(By.CSS_SELECTOR, 'button[type=submit]').click() + + log("Waiting Home page") + wait_title_contains("Home") + + driver.close() + driver.quit() + '' From d1aaa88e289d71d66b66f3d4fb2071a6ceccf3bf Mon Sep 17 00:00:00 2001 From: eljamm Date: Fri, 30 Jan 2026 11:08:47 +0100 Subject: [PATCH 3/3] projects(bonfire): add demo; refactor --- projects/Bonfire/default.nix | 24 +++++++++++++-- projects/Bonfire/demo/module-demo.nix | 24 +++++++++------ projects/Bonfire/demo/module.nix | 5 ---- .../services/bonfire/examples/basic.nix | 30 ++++++++++++++++++- .../Bonfire/services/bonfire/tests/basic.nix | 17 ----------- 5 files changed, 65 insertions(+), 35 deletions(-) delete mode 100644 projects/Bonfire/demo/module.nix diff --git a/projects/Bonfire/default.nix b/projects/Bonfire/default.nix index 43aecc737..194f225c8 100644 --- a/projects/Bonfire/default.nix +++ b/projects/Bonfire/default.nix @@ -7,7 +7,7 @@ { metadata = { - summary = "An open-source framework for building federated digital spaces where people can gather, interact, and form communities online"; + summary = "Open-source framework for building federated digital spaces where people can gather, interact, and form communities online"; subgrants = { Commons = [ ]; Core = [ ]; @@ -15,17 +15,23 @@ "Bonfire-FederatedGroups" "Bonfire-Framework" ]; - Review = [ "Bonfire" ]; + Review = [ + "Bonfire" + ]; }; links = { homepage = { text = "Home page"; url = "https://bonfirenetworks.org"; }; - src = { + repo = { text = "Source code (only the top-level repository)"; url = "https://github.com/bonfire-networks/bonfire-app"; }; + docs = { + text = "Documentation"; + url = "https://docs.bonfirenetworks.org/readme.html"; + }; }; }; @@ -47,4 +53,16 @@ }; }; + nixos.demo.vm = { + module = ./services/bonfire/examples/basic.nix; + module-demo = ./demo/module-demo.nix; + usage-instructions = [ + { + instruction = '' + Wait until the service finishes its setup, then visit [http://127.0.0.1:18000](http://127.0.0.1:18000) in your browser + ''; + } + ]; + tests.demo.module = null; + }; } diff --git a/projects/Bonfire/demo/module-demo.nix b/projects/Bonfire/demo/module-demo.nix index fe8a6105e..eff92a918 100644 --- a/projects/Bonfire/demo/module-demo.nix +++ b/projects/Bonfire/demo/module-demo.nix @@ -4,17 +4,23 @@ ... }: let - cfg = config.programs.bonfire; + cfg = config.services.bonfire; + servicePort = 18000; in { config = lib.mkIf cfg.enable { - demo-shell = { - programs = { - bonfire = cfg.package; - }; - env = { - PROGRAM_PORT = toString cfg.port; - }; - }; + programs.bash.interactiveShellInit = '' + echo "Bonfire is starting. Please wait ..." + until systemctl show bonfire.service | grep -q ActiveState=active; do sleep 1; done + echo "Bonfire is ready at http://localhost:${toString servicePort}" + ''; + + virtualisation.forwardPorts = [ + { + from = "host"; + host.port = servicePort; + guest.port = 80; + } + ]; }; } diff --git a/projects/Bonfire/demo/module.nix b/projects/Bonfire/demo/module.nix deleted file mode 100644 index e275b9566..000000000 --- a/projects/Bonfire/demo/module.nix +++ /dev/null @@ -1,5 +0,0 @@ -{ ... }: - -{ - programs.bonfire.enable = true; -} diff --git a/projects/Bonfire/services/bonfire/examples/basic.nix b/projects/Bonfire/services/bonfire/examples/basic.nix index c352dc4c0..80c341aed 100644 --- a/projects/Bonfire/services/bonfire/examples/basic.nix +++ b/projects/Bonfire/services/bonfire/examples/basic.nix @@ -1,14 +1,21 @@ -{ pkgs, ... }: +{ + pkgs, + ... +}: { networking.domain = "localdomain"; + services.bonfire = { enable = true; + settings = { HOSTNAME = "localhost"; PUBLIC_PORT = 80; }; + postgresql.enable = true; meilisearch.enable = true; + nginx = { enable = true; virtualHost = { @@ -21,5 +28,26 @@ }; }; }; + + # WARN: !! Don't use this in production !! + # Instead, put the secrets directly in the systemd credentials store (`/etc/credstore/`, `/run/credstore/`, ...) + # For more information on this topic, see: + environment.etc = { + # openssl rand -hex 128 + "credstore/bonfire.ENCRYPTION_SALT".text = + "fde9939363a25b2696a7cfd738afcb19f82e2212bca4124d2c70102f3809974c618aeaa279e4daa062b53e07e7d14b4297409a582389a94bac247de13da116d76d6644174d21ad3814ddd7269696997447b8c8fb5f75aa757a8f32148708bb38bf0d66f1dd4a206e9ab3b3818f79dc48303c9375fa68210dbd8567f3a5bcf4f2"; + # openssl rand -hex 25 + "credstore/bonfire.POSTGRES_PASSWORD".text = "ced4a928ed2305630f7865a160b26bc6ab690c445529340fcf"; + # openssl rand -hex 40 + "credstore/bonfire.RELEASE_COOKIE".text = + "1255749c5082f5c64d6984231a02095f6273875363008a0a6ed2c413bbd7ed66249eeebf8abbae3d"; + # openssl rand -hex 128 + "credstore/bonfire.SECRET_KEY_BASE".text = + "0da76ae83b6e2170d3d501ac000dfe96adc820d16cbf54567188f206c9322dcfaf5fac1c5fc6ab742249ff28b69e7b06addc69e02e49290319bb3cc8df0aff920e1f812cf6906ac4711425a7bb7af2f5cf78e03039c8812f04eb2f1ce1ef31a1ff81bc6d4de06ec524171310f6c7fb2ac832f387725842667870081311386b82"; + # openssl rand -hex 128 + "credstore/bonfire.SIGNING_SALT".text = + "3278f788f120031c3d2b8dc480fce1dba38b6ce3f16de17df443e24c66a689d75e52516beec260a3f3bf53e8637c7e66591126e25a526dd25e3e26383124656eb9ad94441c31f278852a55cfe8083e8a0fef6b061fa8c34cbe26169a3dd43854c719c2ad269449fe9172193b031b5f76c16813fb7ec0a195289b6eb5ccfaa1ca"; + }; + services.meilisearch.masterKeyFile = pkgs.writeText "meilisearch.masterKeyFile" "675b2c63f569d0bb3f872517b903fa9ea3ddce19d5766c80a8"; } diff --git a/projects/Bonfire/services/bonfire/tests/basic.nix b/projects/Bonfire/services/bonfire/tests/basic.nix index 537e7f545..bf0743264 100644 --- a/projects/Bonfire/services/bonfire/tests/basic.nix +++ b/projects/Bonfire/services/bonfire/tests/basic.nix @@ -21,23 +21,6 @@ # as soon as running the initial migration of the PostgreSQL schema. virtualisation.memorySize = 4096; - virtualisation.credentials = { - # openssl rand -hex 128 - "bonfire.ENCRYPTION_SALT".text = - "fde9939363a25b2696a7cfd738afcb19f82e2212bca4124d2c70102f3809974c618aeaa279e4daa062b53e07e7d14b4297409a582389a94bac247de13da116d76d6644174d21ad3814ddd7269696997447b8c8fb5f75aa757a8f32148708bb38bf0d66f1dd4a206e9ab3b3818f79dc48303c9375fa68210dbd8567f3a5bcf4f2"; - # openssl rand -hex 25 - "bonfire.POSTGRES_PASSWORD".text = "ced4a928ed2305630f7865a160b26bc6ab690c445529340fcf"; - # openssl rand -hex 40 - "bonfire.RELEASE_COOKIE".text = - "1255749c5082f5c64d6984231a02095f6273875363008a0a6ed2c413bbd7ed66249eeebf8abbae3d"; - # openssl rand -hex 128 - "bonfire.SECRET_KEY_BASE".text = - "0da76ae83b6e2170d3d501ac000dfe96adc820d16cbf54567188f206c9322dcfaf5fac1c5fc6ab742249ff28b69e7b06addc69e02e49290319bb3cc8df0aff920e1f812cf6906ac4711425a7bb7af2f5cf78e03039c8812f04eb2f1ce1ef31a1ff81bc6d4de06ec524171310f6c7fb2ac832f387725842667870081311386b82"; - # openssl rand -hex 128 - "bonfire.SIGNING_SALT".text = - "3278f788f120031c3d2b8dc480fce1dba38b6ce3f16de17df443e24c66a689d75e52516beec260a3f3bf53e8637c7e66591126e25a526dd25e3e26383124656eb9ad94441c31f278852a55cfe8083e8a0fef6b061fa8c34cbe26169a3dd43854c719c2ad269449fe9172193b031b5f76c16813fb7ec0a195289b6eb5ccfaa1ca"; - }; - environment.systemPackages = [ # ToDo: check if those are required here pkgs.firefox-unwrapped