Skip to content

Commit

Permalink
nixos/froide-govplan: init
Browse files Browse the repository at this point in the history
  • Loading branch information
onny committed Jan 7, 2025
1 parent a590472 commit 6200e65
Show file tree
Hide file tree
Showing 5 changed files with 274 additions and 0 deletions.
2 changes: 2 additions & 0 deletions nixos/doc/manual/release-notes/rl-2505.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@

- [Whoogle Search](https://github.com/benbusby/whoogle-search), a self-hosted, ad-free, privacy-respecting metasearch engine. Available as [services.whoogle-search](options.html#opt-services.whoogle-search.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).
Expand Down
1 change: 1 addition & 0 deletions nixos/modules/module-list.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1466,6 +1466,7 @@
./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
Expand Down
234 changes: 234 additions & 0 deletions nixos/modules/services/web-apps/froide-govplan.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
{
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.writeScriptBin "froide-govplan" ''
#! ${pkgs.runtimeShell}
sudo=exec
if [[ "$USER" != govplan ]]; then
sudo='exec /run/wrappers/bin/sudo -u govplan'
fi
$sudo ${pkgs.coreutils}/bin/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 ];

}
1 change: 1 addition & 0 deletions nixos/tests/all-tests.nix
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ in {
freshrss-http-auth = handleTest ./freshrss-http-auth.nix {};
freshrss-none-auth = handleTest ./freshrss-none-auth.nix {};
frigate = handleTest ./frigate.nix {};
froide-govplan = handleTest ./web-apps/froide-govplan.nix {};
frp = handleTest ./frp.nix {};
frr = handleTest ./frr.nix {};
fsck = handleTest ./fsck.nix {};
Expand Down
36 changes: 36 additions & 0 deletions nixos/tests/web-apps/froide-govplan.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import ../make-test-python.nix (
{ lib, pkgs, ... }:
{
name = "froide-govplan";
meta.maintainers = with lib.maintainers; [ onny ];

nodes.machine = { config, ... }: {
virtualisation.memorySize = 2048;
services.froide-govplan.enable = true;
};

testScript = let
changePassword = pkgs.writeText "change-password.py" ''
from users.models import User
u = User.objects.get(username='govplan')
u.set_password('govplan')
u.save()
'';
in ''
start_all()
machine.wait_for_unit("froide-govplan.service")
with subtest("Home screen loads"):
machine.succeed(
"curl -sSfL http://[::1]:8080 | grep '<title>Home | NetBox</title>'"
)
with subtest("Superuser can be created"):
machine.succeed(
"froide-govplan createsuperuser --noinput --username govplan --email govplan@example.com"
)
# Django doesn't have a "clean" way of inputting the password from the command line
machine.succeed("cat '${changePassword}' | netbox-manage shell")
'';
}
)

0 comments on commit 6200e65

Please sign in to comment.