# Copyright Tom Hubrecht, (2023) # # Tom Hubrecht # # This software is a computer program whose purpose is to configure # machines and servers with NixOS. # # This software is governed by the CeCILL license under French law and # abiding by the rules of distribution of free software. You can use, # modify and/ or redistribute the software under the terms of the CeCILL # license as circulated by CEA, CNRS and INRIA at the following URL # "http://www.cecill.info". # # As a counterpart to the access to the source code and rights to copy, # modify and redistribute granted by the license, users are provided only # with a limited warranty and the software's author, the holder of the # economic rights, and the successive licensors have only limited # liability. # # In this respect, the user's attention is drawn to the risks associated # with loading, using, modifying and/or developing or reproducing the # software by the user in light of its specific status of free software, # that may mean that it is complicated to manipulate, and that also # therefore means that it is reserved for developers and experienced # professionals having in-depth computer knowledge. Users are therefore # encouraged to load and test the software's suitability as regards their # requirements in conditions enabling the security of their systems and/or # data to be ensured and, more generally, to use and operate it in the # same conditions as regards security. # # The fact that you are presently reading this means that you have had # knowledge of the CeCILL license and that you accept its terms. { config, lib, pkgs, ... }: let inherit (lib) mdDoc mkDefault mkEnableOption mkIf mkOption optional optionalString types; cfg = config.services.demarches-simplifiees; settingsFormat = pkgs.formats.keyValue { }; env = settingsFormat.generate "ds-fr-env" cfg.settings; ds-fr = pkgs.writeShellScriptBin "ds-fr" '' set -a cd ${cfg.package} ${optionalString (cfg.secretFile != null) "source ${cfg.secretFile}"} source ${env} BIN="$1" shift SUDO="exec" if [[ $USER != ${cfg.user} ]]; then SUDO='exec /run/wrappers/bin/sudo -u ${cfg.user} --preserve-env' fi $SUDO ${cfg.package}/bin/$BIN "$@" ''; in { options.services.demarches-simplifiees = { enable = mkEnableOption "demarches-simplifiees."; package = mkOption { type = types.package; default = pkgs.callPackage ./package { inherit (cfg) initialDeploymentDate dataDir logDir; }; }; user = mkOption { type = types.str; default = "ds-fr"; description = mdDoc "User account under which DS runs."; }; group = mkOption { type = types.str; default = "ds-fr"; description = mdDoc "Group account under which DS runs."; }; dataDir = mkOption { type = types.str; default = "/var/lib/ds-fr"; }; logDir = mkOption { type = types.str; default = "/var/log/ds-fr"; }; secretFile = mkOption { type = types.nullOr types.path; default = null; }; settings = mkOption { inherit (settingsFormat) type; }; initialDeploymentDate = mkOption { type = types.nullOr types.str; default = null; }; }; config = mkIf cfg.enable { services.demarches-simplifiees.settings = (builtins.mapAttrs (_: mkDefault) { RAILS_ENV = "production"; RAILS_ROOT = builtins.toString cfg.package; # Application host name # # Examples: # * For local development: localhost:3000 # * For preproduction: staging.ds.example.org # * For production: ds.example.org APP_HOST = "localhost:3000"; # Rails key for signing sensitive data # See https://guides.rubyonrails.org/security.html # # For production you MUST generate a new key, and keep it secret. # Secrets must be long and random. Use bin/rails secret to get new unique secrets. # Secret key for One-Time-Password codes, used for 2-factors authentication # OTP_SECRET_KEY = ""; # Protect access to the instance with a static login/password (useful for staging environments) BASIC_AUTH_ENABLED = "disabled"; BASIC_AUTH_USERNAME = ""; BASIC_AUTH_PASSWORD = ""; # ActiveStorage service to use for attached files. # Possible values: # - "local": store files on the local filesystem # - "amazon": store files remotely on an S3 storage service # - "openstack": store files remotely on an OpenStack storage service # # (See config/storage.yml for the configuration of each service.) ACTIVE_STORAGE_SERVICE = "local"; # Configuration for the OpenStack storage service (if enabled) FOG_OPENSTACK_API_KEY = ""; FOG_OPENSTACK_USERNAME = ""; FOG_OPENSTACK_URL = ""; FOG_OPENSTACK_REGION = ""; DS_PROXY_URL = ""; # SAML SAML_IDP_ENABLED = "disabled"; # External service: authentication through France Connect FC_PARTICULIER_ID = ""; FC_PARTICULIER_SECRET = ""; FC_PARTICULIER_BASE_URL = ""; # External service: authentication through Agent Connect AGENT_CONNECT_ID = ""; AGENT_CONNECT_SECRET = ""; AGENT_CONNECT_BASE_URL = ""; AGENT_CONNECT_JWKS = ""; AGENT_CONNECT_REDIRECT = ""; # External service: integration with HelpScout (optional) HELPSCOUT_MAILBOX_ID = ""; HELPSCOUT_CLIENT_ID = ""; HELPSCOUT_CLIENT_SECRET = ""; HELPSCOUT_WEBHOOK_SECRET = ""; # External service: external supervision SENTRY_ENABLED = "disabled"; SENTRY_CURRENT_ENV = "development"; SENTRY_DSN_RAILS = ""; SENTRY_DSN_JS = ""; # External service: Matomo web analytics MATOMO_ENABLED = "disabled"; MATOMO_COOKIE_DOMAIN = "*.www.demarches-simplifiees.fr"; MATOMO_DOMAIN = "*.www.demarches-simplifiees.fr"; MATOMO_ID = ""; MATOMO_HOST = "matomo.example.org"; # Default SMTP Provider: Mailjet MAILJET_API_KEY = ""; MAILJET_SECRET_KEY = ""; # Alternate SMTP Provider: SendInBlue/DoList SENDINBLUE_CLIENT_KEY = ""; SENDINBLUE_SMTP_KEY = ""; SENDINBLUE_USER_NAME = ""; # SENDINBLUE_LOGIN_URL="https://app.sendinblue.com/account/saml/login/truc" # Alternate SMTP Provider: Mailtrap (mail catcher for staging environments) # When enabled, all emails will be sent using this provider MAILTRAP_ENABLED = "disabled"; MAILTRAP_USERNAME = ""; MAILTRAP_PASSWORD = ""; # Alternative SMTP Provider: Mailcatcher (Catches mail and serves it through a dream.) # When enabled, all emails will be sent using this provider MAILCATCHER_ENABLED = "disabled"; MAILCATCHER_HOST = ""; MAILCATCHER_PORT = ""; # External service: live chat for admins (specific to démarches-simplifiées.fr) CRISP_ENABLED = "disabled"; CRISP_CLIENT_KEY = ""; # API Entreprise credentials # https://api.gouv.fr/api/api-entreprise.html API_ENTREPRISE_KEY = ""; # External service: CRM for following admin accounts pipeline (specific to démarches-simplifiées.fr) PIPEDRIVE_KEY = ""; # Networks bypassing the email login token that verifies new devices, and rack-attack throttling TRUSTED_NETWORKS = ""; # External service: mesuring performance of the Rails app (specific to démarches-simplifiées.fr) SKYLIGHT_AUTHENTICATION_KEY = ""; # "sXaot-fKhBlkI8qaSirQyuZbrpv5sVFoOturQ0pFEh0"; # Enable or disable Lograge logs LOGRAGE_ENABLED = "disabled"; # Logs source for Lograge # # Examples: # * For local development: tps_local # * For preproduction: tps_staging # * For production: tps_prod LOGRAGE_SOURCE = "tps_prod"; # External service: timestamping a daily archive of dossiers status changes UNIVERSIGN_API_URL = "https://ws.universign.eu/tsa/post/"; UNIVERSIGN_USERPWD = ""; # External service: API Geo / Adresse API_ADRESSE_URL = "https://api-adresse.data.gouv.fr"; API_GEO_URL = "https://geo.api.gouv.fr"; # External service: API Education API_EDUCATION_URL = "https://data.education.gouv.fr/api/records/1.0"; # Encryption key for sensitive columns in the database ENCRYPTION_SERVICE_SALT = ""; # ActiveRecord encryption keys. Generate them with bin/rails db:encryption:init (you can omit deterministic_key) AR_ENCRYPTION_PRIMARY_KEY = ""; AR_ENCRYPTION_KEY_DERIVATION_SALT = ""; # Salt for invisible_captcha session data. # Must be the same value for all app instances behind a load-balancer. INVISIBLE_CAPTCHA_SECRET = "kikooloool"; # Clamav antivirus usage CLAMAV_ENABLED = "disabled"; # Siret number used for API Entreprise, by default we use SIRET from dinum API_ENTREPRISE_DEFAULT_SIRET = "put_your_own_siret"; }) // { # Database credentials DB_DATABASE = "ds-fr"; DB_USERNAME = cfg.user; DB_PASSWORD = ""; DB_HOST = "/run/postgresql"; DB_POOL = ""; # Log on stdout RAILS_LOG_TO_STDOUT = true; }; environment.systemPackages = [ ds-fr ]; systemd.tmpfiles.rules = [ "f '${cfg.logDir}/production.log' 0640 ${cfg.user} ${cfg.group} - -" "f '${cfg.dataDir}/.env' 0600 ${cfg.user} ${cfg.group} - -" "d '${cfg.dataDir}/tmp' 0700 ${cfg.user} ${cfg.group} 10d -" ]; systemd.services = { ds-fr-setup = { description = "Demarches Simplifiees setup"; wantedBy = [ "multi-user.target" ]; path = [ pkgs.bash ds-fr ]; after = [ "postgresql.service" ]; serviceConfig = { Type = "oneshot"; User = cfg.user; Group = cfg.group; EnvironmentFile = [ env ] ++ (optional (cfg.secretFile != null) cfg.secretFile); StateDirectory = mkIf (cfg.dataDir == "/var/lib/ds-fr") "ds-fr"; LogsDirectory = mkIf (cfg.logDir == "/var/log/ds-fr") "ds-fr"; }; script = '' [[ ! -f ${cfg.dataDir}/.initial-migration ]] \ && ds-fr rails db:environment:set \ && ds-fr rails db:schema:load \ && ds-fr rails db:seed \ && touch ${cfg.dataDir}/.initial-migration ds-fr rake db:migrate ds-fr rake after_party:run ''; }; ds-fr-work = { description = "Demarches Simplifiees work service"; wantedBy = [ "multi-user.target" "ds-fr.service" ]; after = [ "network.target" "ds-fr-setup.service" ]; requires = [ "ds-fr-setup.service" ]; serviceConfig = { ExecStart = "${ds-fr}/bin/ds-fr rails jobs:work"; EnvironmentFile = [ env ] ++ (optional (cfg.secretFile != null) cfg.secretFile); User = cfg.user; Group = cfg.group; StateDirectory = mkIf (cfg.dataDir == "/var/lib/ds-fr") "ds-fr"; LogsDirectory = mkIf (cfg.logDir == "/var/log/ds-fr") "ds-fr"; }; }; ds-fr = { description = "Demarches Simplifiees web service"; wantedBy = [ "multi-user.target" ]; after = [ "network.target" "ds-fr-setup.service" ]; requires = [ "ds-fr-setup.service" ]; serviceConfig = { ExecStart = "${ds-fr}/bin/ds-fr rails server"; Environment = [ "RAILS_QUEUE_ADAPTER=delayed_job" ]; EnvironmentFile = [ env ] ++ (optional (cfg.secretFile != null) cfg.secretFile); User = cfg.user; Group = cfg.group; StateDirectory = mkIf (cfg.dataDir == "/var/lib/ds-fr") "ds-fr"; LogsDirectory = mkIf (cfg.logDir == "/var/log/ds-fr") "ds-fr"; }; }; }; services.postgresql = { enable = true; ensureDatabases = [ "ds-fr" ]; ensureUsers = optional (cfg.user == "ds-fr") { name = "ds-fr"; ensurePermissions = { "DATABASE \"ds-fr\"" = "ALL PRIVILEGES"; }; }; extraPlugins = with config.services.postgresql.package.pkgs; [ postgis ]; }; users.users = mkIf (cfg.user == "ds-fr") { ds-fr = { inherit (cfg) group; isSystemUser = true; home = cfg.package; }; }; users.groups.${cfg.group} = { }; services.nginx = { enable = true; virtualHosts.${cfg.settings.APP_HOST} = { enableACME = true; forceSSL = true; root = "${cfg.package}/public/"; locations."/".tryFiles = "$uri @proxy"; locations."@proxy" = { proxyPass = "http://127.0.0.1:3000"; }; }; }; }; }