Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

froide-govplan: init at 0-unstable-2024-09-19 #349750

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
40 changes: 40 additions & 0 deletions nixos/tests/web-apps/froide-govplan.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
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")
'';
}
)
28 changes: 28 additions & 0 deletions pkgs/by-name/fr/froide-govplan/disable_missing_template.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
diff --git a/froide_govplan/templates/froide_govplan/detail.html b/froide_govplan/templates/froide_govplan/detail.html
index 25b780b..3620852 100644
--- a/froide_govplan/templates/froide_govplan/detail.html
+++ b/froide_govplan/templates/froide_govplan/detail.html
@@ -5,7 +5,6 @@
{% load cms_tags %}
{% load follow_tags %}
{% load govplan %}
-{% load form_helper %}
{% load content_helper %}
{% load thumbnail %}

@@ -41,7 +40,6 @@
</ul>

<div class="ms-auto mt-2 mt-md-0">
- {% show_follow "govplan" object %}
</div>
</div>

@@ -177,7 +175,6 @@
<div class="modal-body">
<form method="post" action="{% url 'govplan:propose_planupdate' gov=object.government.slug plan=object.slug %}">
{% csrf_token %}
- {% render_form update_proposal_form %}
<button type="submit" class="btn btn-primary">
Neue Entwicklung melden
</button>
15 changes: 15 additions & 0 deletions pkgs/by-name/fr/froide-govplan/load_extra_settings.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
diff --git a/project/settings.py b/project/settings.py
index dd282ac..64ff265 100644
--- a/project/settings.py
+++ b/project/settings.py
@@ -202,3 +202,10 @@ CMS_CONFIRM_VERSION4 = True

GOVPLAN_NAME = "GovPlan"
GOVPLAN_ENABLE_FOIREQUEST = False
+
+EXTRA_SETTINGS_PATH = os.path.join(PROJECT_DIR, 'extra_settings.py')
+
+if os.path.exists(EXTRA_SETTINGS_PATH):
+ with open(EXTRA_SETTINGS_PATH) as f:
+ code = compile(f.read(), EXTRA_SETTINGS_PATH, 'exec')
+ exec(code)
Loading