nixos/froide-govplan: init

Changed files
+240
nixos
doc
manual
release-notes
modules
+2
nixos/doc/manual/release-notes/rl-2505.section.md
···
- [cross-seed](https://www.cross-seed.org), a tool to set-up fully automatic cross-seeding of torrents. Available as [services.cross-seed](#opt-services.cross-seed.enable).
+
- [Froide-Govplan](https://github.com/okfde/froide-govplan), a web application government planer. Available as [services.froide-govplan](#opt-services.froide-govplan.enable).
+
- [agorakit](https://github.com/agorakit/agorakit), an organization tool for citizens' collectives. Available with [services.agorakit](options.html#opt-services.agorakit.enable).
- [vivid](https://github.com/sharkdp/vivid), a generator for LS_COLOR. Available as [programs.vivid](#opt-programs.vivid.enable).
+1
nixos/modules/module-list.nix
···
./services/web-apps/flarum.nix
./services/web-apps/fluidd.nix
./services/web-apps/freshrss.nix
+
./services/web-apps/froide-govplan.nix
./services/web-apps/galene.nix
./services/web-apps/gancio.nix
./services/web-apps/gerrit.nix
+237
nixos/modules/services/web-apps/froide-govplan.nix
···
+
{
+
config,
+
lib,
+
pkgs,
+
...
+
}:
+
let
+
+
cfg = config.services.froide-govplan;
+
pythonFmt = pkgs.formats.pythonVars { };
+
settingsFile = pythonFmt.generate "extra_settings.py" cfg.settings;
+
+
pkg = cfg.package.overridePythonAttrs (old: {
+
postInstall =
+
old.postInstall
+
+ ''
+
ln -s ${settingsFile} $out/${pkg.python.sitePackages}/froide_govplan/project/extra_settings.py
+
'';
+
});
+
+
froide-govplan = pkgs.writeShellApplication {
+
name = "froide-govplan";
+
runtimeInputs = [ pkgs.coreutils ];
+
text = ''
+
SUDO="exec"
+
if [[ "$USER" != govplan ]]; then
+
SUDO="exec /run/wrappers/bin/sudo -u govplan"
+
fi
+
$SUDO env ${lib.getExe pkg} "$@"
+
'';
+
};
+
+
# Service hardening
+
defaultServiceConfig = {
+
# Secure the services
+
ReadWritePaths = [ cfg.dataDir ];
+
CacheDirectory = "froide-govplan";
+
CapabilityBoundingSet = "";
+
# ProtectClock adds DeviceAllow=char-rtc r
+
DeviceAllow = "";
+
LockPersonality = true;
+
MemoryDenyWriteExecute = true;
+
NoNewPrivileges = true;
+
PrivateDevices = true;
+
PrivateMounts = true;
+
PrivateTmp = true;
+
PrivateUsers = true;
+
ProtectClock = true;
+
ProtectHome = true;
+
ProtectHostname = true;
+
ProtectSystem = "strict";
+
ProtectControlGroups = true;
+
ProtectKernelLogs = true;
+
ProtectKernelModules = true;
+
ProtectKernelTunables = true;
+
ProtectProc = "invisible";
+
ProcSubset = "pid";
+
RestrictAddressFamilies = [
+
"AF_UNIX"
+
"AF_INET"
+
"AF_INET6"
+
];
+
RestrictNamespaces = true;
+
RestrictRealtime = true;
+
RestrictSUIDSGID = true;
+
SystemCallArchitectures = "native";
+
SystemCallFilter = [
+
"@system-service"
+
"~@privileged @setuid @keyring"
+
];
+
UMask = "0066";
+
};
+
+
in
+
{
+
options.services.froide-govplan = {
+
+
enable = lib.mkEnableOption "Gouvernment planer web app Govplan";
+
+
package = lib.mkPackageOption pkgs "froide-govplan" { };
+
+
hostName = lib.mkOption {
+
type = lib.types.str;
+
default = "localhost";
+
description = "FQDN for the froide-govplan instance.";
+
};
+
+
dataDir = lib.mkOption {
+
type = lib.types.str;
+
default = "/var/lib/froide-govplan";
+
description = "Directory to store the Froide-Govplan server data.";
+
};
+
+
secretKeyFile = lib.mkOption {
+
type = lib.types.nullOr lib.types.path;
+
default = null;
+
description = ''
+
Path to a file containing the secret key.
+
'';
+
};
+
+
settings = lib.mkOption {
+
description = ''
+
Configuration options to set in `extra_settings.py`.
+
'';
+
+
default = { };
+
+
type = lib.types.submodule {
+
freeformType = pythonFmt.type;
+
+
options = {
+
ALLOWED_HOSTS = lib.mkOption {
+
type = with lib.types; listOf str;
+
default = [ "*" ];
+
description = ''
+
A list of valid fully-qualified domain names (FQDNs) and/or IP
+
addresses that can be used to reach the Froide-Govplan service.
+
'';
+
};
+
};
+
};
+
};
+
+
};
+
+
config = lib.mkIf cfg.enable {
+
+
services.froide-govplan = {
+
settings = {
+
STATIC_ROOT = "${cfg.dataDir}/static";
+
DEBUG = false;
+
DATABASES.default = {
+
ENGINE = "django.contrib.gis.db.backends.postgis";
+
NAME = "govplan";
+
USER = "govplan";
+
HOST = "/run/postgresql";
+
};
+
};
+
};
+
+
services.postgresql = {
+
enable = true;
+
ensureDatabases = [ "govplan" ];
+
ensureUsers = [
+
{
+
name = "govplan";
+
ensureDBOwnership = true;
+
}
+
];
+
extensions = ps: with ps; [ postgis ];
+
};
+
+
services.nginx = {
+
enable = lib.mkDefault true;
+
virtualHosts."${cfg.hostName}".locations = {
+
"/".extraConfig = "proxy_pass http://unix:/run/froide-govplan/froide-govplan.socket;";
+
"/static/".alias = "${cfg.dataDir}/static/";
+
};
+
proxyTimeout = lib.mkDefault "120s";
+
};
+
+
systemd = {
+
services = {
+
+
postgresql.serviceConfig.ExecStartPost =
+
let
+
sqlFile = pkgs.writeText "immich-pgvectors-setup.sql" ''
+
CREATE EXTENSION IF NOT EXISTS postgis;
+
'';
+
in
+
[
+
''
+
${lib.getExe' config.services.postgresql.package "psql"} -d govplan -f "${sqlFile}"
+
''
+
];
+
+
froide-govplan = {
+
description = "Gouvernment planer Govplan";
+
serviceConfig = defaultServiceConfig // {
+
WorkingDirectory = cfg.dataDir;
+
StateDirectory = lib.mkIf (cfg.dataDir == "/var/lib/froide-govplan") "froide-govplan";
+
User = "govplan";
+
Group = "govplan";
+
};
+
after = [
+
"postgresql.service"
+
"network.target"
+
"systemd-tmpfiles-setup.service"
+
];
+
wantedBy = [ "multi-user.target" ];
+
environment =
+
{
+
PYTHONPATH = pkg.pythonPath;
+
GDAL_LIBRARY_PATH = "${pkgs.gdal}/lib/libgdal.so";
+
GEOS_LIBRARY_PATH = "${pkgs.geos}/lib/libgeos_c.so";
+
}
+
// lib.optionalAttrs (cfg.secretKeyFile != null) {
+
SECRET_KEY_FILE = cfg.secretKeyFile;
+
};
+
preStart = ''
+
# Auto-migrate on first run or if the package has changed
+
versionFile="${cfg.dataDir}/src-version"
+
version=$(cat "$versionFile" 2>/dev/null || echo 0)
+
+
if [[ $version != ${pkg.version} ]]; then
+
${lib.getExe pkg} migrate --no-input
+
${lib.getExe pkg} collectstatic --no-input --clear
+
echo ${pkg.version} > "$versionFile"
+
fi
+
'';
+
script = ''
+
${pkg.python.pkgs.uvicorn}/bin/uvicorn --uds /run/froide-govplan/froide-govplan.socket \
+
--app-dir ${pkg}/${pkg.python.sitePackages}/froide_govplan \
+
project.asgi:application
+
'';
+
};
+
};
+
+
};
+
+
systemd.tmpfiles.rules = [ "d /run/froide-govplan - govplan govplan - -" ];
+
+
environment.systemPackages = [ froide-govplan ];
+
+
users.users.govplan = {
+
home = "${cfg.dataDir}";
+
isSystemUser = true;
+
group = "govplan";
+
};
+
users.groups.govplan = { };
+
+
};
+
+
meta.maintainers = with lib.maintainers; [ onny ];
+
+
}