-
-
Notifications
You must be signed in to change notification settings - Fork 14.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
274 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ]; | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
''; | ||
} | ||
) |