diff options
39 files changed, 715 insertions, 140 deletions
diff --git a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml index d434b76da214..252f727b7fc1 100644 --- a/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml +++ b/nixos/doc/manual/from_md/release-notes/rl-2211.section.xml @@ -245,6 +245,14 @@ </listitem> <listitem> <para> + <link xlink:href="https://www.dolibarr.org/">Dolibarr</link>, + an enterprise resource planning and customer relationship + manager. Enable using + <link linkend="opt-services.dolibarr.enable">services.dolibarr</link>. + </para> + </listitem> + <listitem> + <para> <link xlink:href="https://www.expressvpn.com">expressvpn</link>, the CLI client for ExpressVPN. Available as <link linkend="opt-services.expressvpn.enable">services.expressvpn</link>. diff --git a/nixos/doc/manual/release-notes/rl-2211.section.md b/nixos/doc/manual/release-notes/rl-2211.section.md index a90e0f896eca..2e7b734b348e 100644 --- a/nixos/doc/manual/release-notes/rl-2211.section.md +++ b/nixos/doc/manual/release-notes/rl-2211.section.md @@ -88,6 +88,8 @@ In addition to numerous new and upgraded packages, this release has the followin - [schleuder](https://schleuder.org/), a mailing list manager with PGP support. Enable using [services.schleuder](#opt-services.schleuder.enable). +- [Dolibarr](https://www.dolibarr.org/), an enterprise resource planning and customer relationship manager. Enable using [services.dolibarr](#opt-services.dolibarr.enable). + - [expressvpn](https://www.expressvpn.com), the CLI client for ExpressVPN. Available as [services.expressvpn](#opt-services.expressvpn.enable). - [Grafana Tempo](https://www.grafana.com/oss/tempo/), a distributed tracing store. Available as [services.tempo](#opt-services.tempo.enable). diff --git a/nixos/modules/config/no-x-libs.nix b/nixos/modules/config/no-x-libs.nix index 42e68c2eadc1..946c9bf38c47 100644 --- a/nixos/modules/config/no-x-libs.nix +++ b/nixos/modules/config/no-x-libs.nix @@ -27,9 +27,13 @@ with lib; fonts.fontconfig.enable = false; nixpkgs.overlays = singleton (const (super: { + beam = super.beam_nox; cairo = super.cairo.override { x11Support = false; }; dbus = super.dbus.override { x11Support = false; }; - beam = super.beam_nox; + ffmpeg_4 = super.ffmpeg_4.override { sdlSupport = false; vdpauSupport = false; }; + ffmpeg_5 = super.ffmpeg_5.override { sdlSupport = false; vdpauSupport = false; }; + gobject-introspection = super.gobject-introspection.override { x11Support = false; }; + libva = super.libva-minimal; networkmanager-fortisslvpn = super.networkmanager-fortisslvpn.override { withGnome = false; }; networkmanager-iodine = super.networkmanager-iodine.override { withGnome = false; }; networkmanager-l2tp = super.networkmanager-l2tp.override { withGnome = false; }; @@ -37,7 +41,6 @@ with lib; networkmanager-openvpn = super.networkmanager-openvpn.override { withGnome = false; }; networkmanager-sstp = super.networkmanager-vpnc.override { withGnome = false; }; networkmanager-vpnc = super.networkmanager-vpnc.override { withGnome = false; }; - gobject-introspection = super.gobject-introspection.override { x11Support = false; }; qemu = super.qemu.override { gtkSupport = false; spiceSupport = false; sdlSupport = false; }; })); }; diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 132bbdcd933b..12692d7bfbe6 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1065,6 +1065,7 @@ ./services/web-apps/discourse.nix ./services/web-apps/documize.nix ./services/web-apps/dokuwiki.nix + ./services/web-apps/dolibarr.nix ./services/web-apps/engelsystem.nix ./services/web-apps/ethercalc.nix ./services/web-apps/fluidd.nix diff --git a/nixos/modules/services/networking/minidlna.nix b/nixos/modules/services/networking/minidlna.nix index 0cac41f58da3..549f1fe5de30 100644 --- a/nixos/modules/services/networking/minidlna.nix +++ b/nixos/modules/services/networking/minidlna.nix @@ -1,6 +1,5 @@ # Module for MiniDLNA, a simple DLNA server. { config, lib, pkgs, ... }: - with lib; let @@ -34,8 +33,7 @@ in default = {}; description = lib.mdDoc '' The contents of MiniDLNA's configuration file. - When the service is activated, a basic template is generated - from the current options opened here. + When the service is activated, a basic template is generated from the current options opened here. ''; type = types.submodule { freeformType = settingsFormat.type; @@ -46,10 +44,8 @@ in example = [ "/data/media" "V,/home/alice/video" ]; description = lib.mdDoc '' Directories to be scanned for media files. - The prefixes `A,`,`V,` and - `P,` restrict a directory to audio, video - or image files. The directories must be accessible to the - `minidlna` user account. + The `A,` `V,` `P,` prefixes restrict a directory to audio, video or image files. + The directories must be accessible to the `minidlna` user account. ''; }; options.notify_interval = mkOption { @@ -57,18 +53,11 @@ in default = 90000; description = lib.mdDoc '' The interval between announces (in seconds). - Instead of waiting on announces, one can open port UDP 1900 or - set `openFirewall` option to use SSDP discovery. - Furthermore announce interval has now been set as 90000 in order - to prevent disconnects with certain clients and to rely solely - on the SSDP method. - - Lower values (e.g. 60 seconds) should be used if one does not - want to utilize SSDP. By default miniDLNA will announce its - presence on the network approximately every 15 minutes. Many - people prefer shorter announce intervals on their home networks, - especially when DLNA clients are started on demand. + Instead of waiting for announces, you should set `openFirewall` option to use SSDP discovery. + Furthermore, this option has been set to 90000 in order to prevent disconnects with certain + clients and relies solely on the discovery. + Lower values (e.g. 30 seconds) should be used if you can't use the discovery. Some relevant information can be found here: https://sourceforge.net/p/minidlna/discussion/879957/thread/1389d197/ ''; @@ -86,7 +75,7 @@ in }; options.friendly_name = mkOption { type = types.str; - default = "${config.networking.hostName} MiniDLNA"; + default = config.networking.hostName; defaultText = literalExpression "config.networking.hostName"; example = "rpi3"; description = lib.mdDoc "Name that the DLNA server presents to clients."; @@ -116,7 +105,7 @@ in options.wide_links = mkOption { type = types.enum [ "yes" "no" ]; default = "no"; - description = lib.mdDoc "Set this to yes to allow symlinks that point outside user-defined media_dirs."; + description = lib.mdDoc "Set this to yes to allow symlinks that point outside user-defined `media_dir`."; }; }; }; diff --git a/nixos/modules/services/web-apps/dolibarr.nix b/nixos/modules/services/web-apps/dolibarr.nix new file mode 100644 index 000000000000..2b2e2a6214dc --- /dev/null +++ b/nixos/modules/services/web-apps/dolibarr.nix @@ -0,0 +1,320 @@ +{ config, pkgs, lib, ... }: +let + inherit (lib) any boolToString concatStringsSep isBool isString literalExpression mapAttrsToList mkDefault mkEnableOption mkIf mkOption optionalAttrs types; + + package = pkgs.dolibarr.override { inherit (cfg) stateDir; }; + + cfg = config.services.dolibarr; + vhostCfg = config.services.nginx.virtualHosts."${cfg.domain}"; + + mkConfigFile = filename: settings: + let + # hack in special logic for secrets so we read them from a separate file avoiding the nix store + secretKeys = [ "force_install_databasepass" "dolibarr_main_db_pass" "dolibarr_main_instance_unique_id" ]; + + toStr = k: v: + if (any (str: k == str) secretKeys) then v + else if isString v then "'${v}'" + else if isBool v then boolToString v + else if isNull v then "null" + else toString v + ; + in + pkgs.writeText filename '' + <?php + ${concatStringsSep "\n" (mapAttrsToList (k: v: "\$${k} = ${toStr k v};") settings)} + ''; + + # see https://github.com/Dolibarr/dolibarr/blob/develop/htdocs/install/install.forced.sample.php for all possible values + install = { + force_install_noedit = 2; + force_install_main_data_root = "${cfg.stateDir}/documents"; + force_install_nophpinfo = true; + force_install_lockinstall = "444"; + force_install_distrib = "nixos"; + force_install_type = "mysqli"; + force_install_dbserver = cfg.database.host; + force_install_port = toString cfg.database.port; + force_install_database = cfg.database.name; + force_install_databaselogin = cfg.database.user; + + force_install_mainforcehttps = vhostCfg.forceSSL; + force_install_createuser = false; + force_install_dolibarrlogin = null; + } // optionalAttrs (cfg.database.passwordFile != null) { + force_install_databasepass = ''file_get_contents("${cfg.database.passwordFile}")''; + }; +in +{ + # interface + options.services.dolibarr = { + enable = mkEnableOption "dolibarr"; + + domain = mkOption { + type = types.str; + default = "localhost"; + description = '' + Domain name of your server. + ''; + }; + + user = mkOption { + type = types.str; + default = "dolibarr"; + description = '' + User account under which dolibarr runs. + + <note><para> + If left as the default value this user will automatically be created + on system activation, otherwise you are responsible for + ensuring the user exists before the dolibarr application starts. + </para></note> + ''; + }; + + group = mkOption { + type = types.str; + default = "dolibarr"; + description = '' + Group account under which dolibarr runs. + + <note><para> + If left as the default value this group will automatically be created + on system activation, otherwise you are responsible for + ensuring the group exists before the dolibarr application starts. + </para></note> + ''; + }; + + stateDir = mkOption { + type = types.str; + default = "/var/lib/dolibarr"; + description = '' + State and configuration directory dolibarr will use. + ''; + }; + + database = { + host = mkOption { + type = types.str; + default = "localhost"; + description = "Database host address."; + }; + port = mkOption { + type = types.port; + default = 3306; + description = "Database host port."; + }; + name = mkOption { + type = types.str; + default = "dolibarr"; + description = "Database name."; + }; + user = mkOption { + type = types.str; + default = "dolibarr"; + description = "Database username."; + }; + passwordFile = mkOption { + type = with types; nullOr path; + default = null; + example = "/run/keys/dolibarr-dbpassword"; + description = "Database password file."; + }; + createLocally = mkOption { + type = types.bool; + default = true; + description = "Create the database and database user locally."; + }; + }; + + settings = mkOption { + type = with types; (attrsOf (oneOf [ bool int str ])); + default = { }; + description = lib.mdDoc "Dolibarr settings, see <https://github.com/Dolibarr/dolibarr/blob/develop/htdocs/conf/conf.php.example> for details."; + }; + + nginx = mkOption { + type = types.nullOr (types.submodule ( + lib.recursiveUpdate + (import ../web-servers/nginx/vhost-options.nix { inherit config lib; }) + { + # enable encryption by default, + # as sensitive login and Dolibarr (ERP) data should not be transmitted in clear text. + options.forceSSL.default = true; + options.enableACME.default = true; + } + )); + default = null; + example = lib.literalExpression '' + { + serverAliases = [ + "dolibarr.''${config.networking.domain}" + "erp.''${config.networking.domain}" + ]; + enableACME = false; + } + ''; + description = lib.mdDoc '' + With this option, you can customize an nginx virtual host which already has sensible defaults for Dolibarr. + Set to {} if you do not need any customization to the virtual host. + If enabled, then by default, the {option}`serverName` is + `''${domain}`, + SSL is active, and certificates are acquired via ACME. + If this is set to null (the default), no nginx virtualHost will be configured. + ''; + }; + + poolConfig = mkOption { + type = with types; attrsOf (oneOf [ str int bool ]); + default = { + "pm" = "dynamic"; + "pm.max_children" = 32; + "pm.start_servers" = 2; + "pm.min_spare_servers" = 2; + "pm.max_spare_servers" = 4; + "pm.max_requests" = 500; + }; + description = lib.mdDoc '' + Options for the Dolibarr PHP pool. See the documentation on [`php-fpm.conf`](https://www.php.net/manual/en/install.fpm.configuration.php) + for details on configuration directives. + ''; + }; + }; + + # implementation + config = mkIf cfg.enable { + + assertions = [ + { assertion = cfg.database.createLocally -> cfg.database.user == cfg.user; + message = "services.dolibarr.database.user must match services.dolibarr.user if the database is to be automatically provisioned"; + } + ]; + + services.dolibarr.settings = { + dolibarr_main_url_root = "https://${cfg.domain}"; + dolibarr_main_document_root = "${package}/htdocs"; + dolibarr_main_url_root_alt = "/custom"; + dolibarr_main_data_root = "${cfg.stateDir}/documents"; + + dolibarr_main_db_host = cfg.database.host; + dolibarr_main_db_port = toString cfg.database.port; + dolibarr_main_db_name = cfg.database.name; + dolibarr_main_db_prefix = "llx_"; + dolibarr_main_db_user = cfg.database.user; + dolibarr_main_db_pass = mkIf (cfg.database.passwordFile != null) '' + file_get_contents("${cfg.database.passwordFile}") + ''; + dolibarr_main_db_type = "mysqli"; + dolibarr_main_db_character_set = mkDefault "utf8"; + dolibarr_main_db_collation = mkDefault "utf8_unicode_ci"; + + # Authentication settings + dolibarr_main_authentication = mkDefault "dolibarr"; + + # Security settings + dolibarr_main_prod = true; + dolibarr_main_force_https = vhostCfg.forceSSL; + dolibarr_main_restrict_os_commands = "${pkgs.mariadb}/bin/mysqldump, ${pkgs.mariadb}/bin/mysql"; + dolibarr_nocsrfcheck = false; + dolibarr_main_instance_unique_id = '' + file_get_contents("${cfg.stateDir}/dolibarr_main_instance_unique_id") + ''; + dolibarr_mailing_limit_sendbyweb = false; + }; + + systemd.tmpfiles.rules = [ + "d '${cfg.stateDir}' 0750 ${cfg.user} ${cfg.group}" + "d '${cfg.stateDir}/documents' 0750 ${cfg.user} ${cfg.group}" + "f '${cfg.stateDir}/conf.php' 0660 ${cfg.user} ${cfg.group}" + "L '${cfg.stateDir}/install.forced.php' - ${cfg.user} ${cfg.group} - ${mkConfigFile "install.forced.php" install}" + ]; + + services.mysql = mkIf cfg.database.createLocally { + enable = mkDefault true; + package = mkDefault pkgs.mariadb; + ensureDatabases = [ cfg.database.name ]; + ensureUsers = [ + { name = cfg.database.user; + ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }; + } + ]; + }; + + services.nginx.enable = mkIf (cfg.nginx != null) true; + services.nginx.virtualHosts."${cfg.domain}" = mkIf (cfg.nginx != null) (lib.mkMerge [ + cfg.nginx + ({ + root = lib.mkForce "${package}/htdocs"; + locations."/".index = "index.php"; + locations."~ [^/]\\.php(/|$)" = { + extraConfig = '' + fastcgi_split_path_info ^(.+?\.php)(/.*)$; + fastcgi_pass unix:${config.services.phpfpm.pools.dolibarr.socket}; + ''; + }; + }) + ]); + + systemd.services."phpfpm-dolibarr".after = mkIf cfg.database.createLocally [ "mysql.service" ]; + services.phpfpm.pools.dolibarr = { + inherit (cfg) user group; + phpPackage = pkgs.php.buildEnv { + extensions = { enabled, all }: enabled ++ [ all.calendar ]; + # recommended by dolibarr web application + extraConfig = '' + session.use_strict_mode = 1 + session.cookie_samesite = "Lax" + ; open_basedir = "${package}/htdocs, ${cfg.stateDir}" + allow_url_fopen = 0 + disable_functions = "pcntl_alarm, pcntl_fork, pcntl_waitpid, pcntl_wait, pcntl_wifexited, pcntl_wifstopped, pcntl_wifsignaled, pcntl_wifcontinued, pcntl_wexitstatus, pcntl_wtermsig, pcntl_wstopsig, pcntl_signal, pcntl_signal_get_handler, pcntl_signal_dispatch, pcntl_get_last_error, pcntl_strerror, pcntl_sigprocmask, pcntl_sigwaitinfo, pcntl_sigtimedwait, pcntl_exec, pcntl_getpriority, pcntl_setpriority, pcntl_async_signals" + ''; + }; + + settings = { + "listen.mode" = "0660"; + "listen.owner" = cfg.user; + "listen.group" = cfg.group; + } // cfg.poolConfig; + }; + + # there are several challenges with dolibarr and NixOS which we can address here + # - the dolibarr installer cannot be entirely automated, though it can partially be by including a file called install.forced.php + # - the dolibarr installer requires write access to its config file during installation, though not afterwards + # - the dolibarr config file generally holds secrets generated by the installer, though the config file is a php file so we can read and write these secrets from an external file + systemd.services.dolibarr-config = { + description = "dolibarr configuration file management via NixOS"; + wantedBy = [ "multi-user.target" ]; + + script = '' + # extract the 'main instance unique id' secret that the dolibarr installer generated for us, store it in a file for use by our own NixOS generated configuration file + ${pkgs.php}/bin/php -r "include '${cfg.stateDir}/conf.php'; file_put_contents('${cfg.stateDir}/dolibarr_main_instance_unique_id', \$dolibarr_main_instance_unique_id);" + + # replace configuration file generated by installer with the NixOS generated configuration file + install -m 644 ${mkConfigFile "conf.php" cfg.settings} '${cfg.stateDir}/conf.php' + ''; + + serviceConfig = { + Type = "oneshot"; + User = cfg.user; + Group = cfg.group; + RemainAfterExit = "yes"; + }; + + unitConfig = { + ConditionFileNotEmpty = "${cfg.stateDir}/conf.php"; + }; + }; + + users.users.dolibarr = mkIf (cfg.user == "dolibarr" ) { + isSystemUser = true; + group = cfg.group; + }; + + users.groups = optionalAttrs (cfg.group == "dolibarr") { + dolibarr = { }; + }; + + users.users."${config.services.nginx.group}".extraGroups = [ cfg.group ]; + }; +} diff --git a/nixos/tests/dolibarr.nix b/nixos/tests/dolibarr.nix new file mode 100644 index 000000000000..2f012a0c67da --- /dev/null +++ b/nixos/tests/dolibarr.nix @@ -0,0 +1,59 @@ +import ./make-test-python.nix ({ pkgs, lib, ... }: { + name = "dolibarr"; + meta.maintainers = [ lib.maintainers.raitobezarius ]; + + nodes.machine = + { ... }: + { + services.dolibarr = { + enable = true; + domain = "localhost"; + nginx = { + forceSSL = false; + enableACME = false; + }; + }; + + networking.firewall.allowedTCPPorts = [ 80 ]; + }; + + testScript = '' + from html.parser import HTMLParser + start_all() + + csrf_token = None + class TokenParser(HTMLParser): + def handle_starttag(self, tag, attrs): + attrs = dict(attrs) # attrs is an assoc list originally + if tag == 'input' and attrs.get('name') == 'token': + csrf_token = attrs.get('value') + print(f'[+] Caught CSRF token: {csrf_token}') + def handle_endtag(self, tag): pass + def handle_data(self, data): pass + + machine.wait_for_unit("phpfpm-dolibarr.service") + machine.wait_for_unit("nginx.service") + machine.wait_for_open_port(80) + # Sanity checks on URLs. + # machine.succeed("curl -fL http://localhost/index.php") + # machine.succeed("curl -fL http://localhost/") + # Perform installation. + machine.succeed('curl -fL -X POST http://localhost/install/check.php -F selectlang=auto') + machine.succeed('curl -fL -X POST http://localhost/install/fileconf.php -F selectlang=auto') + # First time is to write the configuration file correctly. + machine.succeed('curl -fL -X POST http://localhost/install/step1.php -F "testpost=ok" -F "action=set" -F "selectlang=auto"') + # Now, we have a proper conf.php in $stateDir. + assert 'nixos' in machine.succeed("cat /var/lib/dolibarr/conf.php") + machine.succeed('curl -fL -X POST http://localhost/install/step2.php --data "testpost=ok&action=set&dolibarr_main_db_character_set=utf8&dolibarr_main_db_collation=utf8_unicode_ci&selectlang=auto"') + machine.succeed('curl -fL -X POST http://localhost/install/step4.php --data "testpost=ok&action=set&selectlang=auto"') + machine.succeed('curl -fL -X POST http://localhost/install/step5.php --data "testpost=ok&action=set&login=root&pass=hunter2&pass_verif=hunter2&selectlang=auto"') + # Now, we have installed the machine, let's verify we still have the right configuration. + assert 'nixos' in machine.succeed("cat /var/lib/dolibarr/conf.php") + # We do not want any redirect now as we have installed the machine. + machine.succeed('curl -f -X POST http://localhost') + # Test authentication to the webservice. + parser = TokenParser() + parser.feed(machine.succeed('curl -f -X GET http://localhost/index.php?mainmenu=login&username=root')) + machine.succeed(f'curl -f -X POST http://localhost/index.php?mainmenu=login&token={csrf_token}&username=root&password=hunter2') + ''; +}) diff --git a/nixos/tests/minidlna.nix b/nixos/tests/minidlna.nix index 76039b0bb42c..32721819634e 100644 --- a/nixos/tests/minidlna.nix +++ b/nixos/tests/minidlna.nix @@ -6,25 +6,24 @@ import ./make-test-python.nix ({ pkgs, ... }: { { ... }: { imports = [ ../modules/profiles/minimal.nix ]; - networking.firewall.allowedTCPPorts = [ 8200 ]; - services.minidlna = { - enable = true; - loglevel = "error"; - mediaDirs = [ - "PV,/tmp/stuff" + services.minidlna.enable = true; + services.minidlna.openFirewall = true; + services.minidlna.settings = { + log_level = "error"; + media_dir = [ + "PV,/tmp/stuff" + ]; + friend |