test(ntfy-sh): hack! direct import waiting for nix-pkgs
Some checks failed
Check meta / check_meta (pull_request) Successful in 19s
Check meta / check_dns (pull_request) Successful in 19s
Check workflows / check_workflows (pull_request) Successful in 19s
Run pre-commit on all files / pre-commit (push) Failing after 28s
Build all the nodes / netcore00 (pull_request) Successful in 27s
Build all the nodes / netaccess01 (pull_request) Successful in 28s
Build all the nodes / netcore01 (pull_request) Successful in 28s
Run pre-commit on all files / pre-commit (pull_request) Failing after 34s
Build all the nodes / ap01 (pull_request) Successful in 42s
Build all the nodes / netcore02 (pull_request) Successful in 30s
Build all the nodes / hypervisor01 (pull_request) Successful in 57s
Build all the nodes / hypervisor03 (pull_request) Successful in 1m3s
Build all the nodes / bridge01 (pull_request) Successful in 1m6s
Build all the nodes / geo02 (pull_request) Successful in 1m6s
Build all the nodes / geo01 (pull_request) Successful in 1m6s
Build all the nodes / cof02 (pull_request) Successful in 1m7s
Build all the nodes / lab-router01 (pull_request) Successful in 1m7s
Build all the nodes / hypervisor02 (pull_request) Successful in 1m8s
Build all the nodes / build01 (pull_request) Successful in 1m15s
Build the shell / build-shell (pull_request) Failing after 15s
Build all the nodes / iso (pull_request) Successful in 1m22s
Build all the nodes / compute01 (pull_request) Successful in 1m25s
Build all the nodes / tower01 (pull_request) Successful in 57s
Build all the nodes / rescue01 (pull_request) Successful in 1m7s
Build all the nodes / vault01 (pull_request) Successful in 1m4s
Build all the nodes / web02 (pull_request) Successful in 54s
Build all the nodes / krz01 (pull_request) Successful in 1m40s
Build all the nodes / storage01 (pull_request) Successful in 1m16s
Build all the nodes / web03 (pull_request) Successful in 50s
Build all the nodes / web01 (pull_request) Failing after 1m40s

This commit is contained in:
catvayor 2025-05-16 22:01:01 +02:00
parent f51b01ee8c
commit 3b3b61d795
Signed by: lbailly
GPG key ID: CE3E645251AC63F3
3 changed files with 235 additions and 1 deletions

View file

@ -39,6 +39,7 @@
"extranix" "extranix"
"openbao" "openbao"
"forgejo-multiuser-nix-runners" "forgejo-multiuser-nix-runners"
"ntfy-sh"
]) ])
++ [ ++ [
"${sources.agenix}/modules/age.nix" "${sources.agenix}/modules/age.nix"
@ -55,7 +56,6 @@
"services/systemd-notify" "services/systemd-notify"
"services/victorialogs" "services/victorialogs"
"services/victoriametrics" "services/victoriametrics"
"services/ntfy-sh"
] ]
++ nodeMeta.nix-modules ++ nodeMeta.nix-modules
)); ));

View file

@ -0,0 +1,148 @@
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
mapAttrsToList
mkEnableOption
mkIf
mkOption
xor
;
inherit (lib.types)
attrsOf
enum
listOf
nullOr
path
str
submodule
;
cfg = config.services.ntfy-sh.accessControl;
inherit (config.services.ntfy-sh) settings;
acl_file = (pkgs.formats.json { }).generate "acl.json" { inherit (cfg) access users; };
ntfy-acl = pkgs.substituteAll {
inherit (pkgs) python3;
inherit acl_file;
user_db = settings.auth-file;
name = "ntfy-acl";
src = ./ntfy-acl.py;
dir = "bin";
isExecutable = true;
};
in
{
options.services.ntfy-sh.accessControl = {
enable = mkEnableOption "declarative management of users and acl for ntfy.sh";
users = mkOption {
type = attrsOf (submodule {
options = {
role = mkOption {
type = enum [
"admin"
"user"
];
description = "Role of the user.";
default = "user";
};
passwordFile = mkOption {
type = nullOr path;
description = ''
Path to a file containing the password of the user.
Conflicts with `hashedPassword`.
'';
default = null;
};
hashedPassword = mkOption {
type = nullOr str;
description = ''
Hashed password of the user.
Conflicts with `passwordFile`.
'';
default = null;
};
};
});
description = ''
Attribute set defining users of the ntfy.sh instance.
'';
default = { };
};
access = mkOption {
type = listOf (submodule {
options = {
username = mkOption {
type = str;
description = ''
A USERNAME is an existing user, as created with ntfy user add (see users and roles),
or the anonymous user everyone or *, which represents clients that access the API
without username/password.
'';
default = "*";
};
topic = mkOption {
type = str;
description = ''
A TOPIC is either a specific topic name (e.g. mytopic, or phil_alerts),
or a wildcard pattern that matches any number of topics (e.g. alerts_* or ben-*).
Only the wildcard character * is supported.
It stands for zero to any number of characters.
'';
};
permission = mkOption {
type = enum [
"rw"
"ro"
"wo"
"none"
];
description = ''
Permission for this access.
- rw: Allows publishing messages to the given topic, as well as subscribing and reading messages
- ro: Allows only subscribing and reading messages, but not publishing to the topic
- wo: Allows only publishing to the topic, but not subscribing to it
- none: Allows neither publishing nor subscribing to a topic
'';
};
};
});
description = "List of access control rules.";
default = [ ];
};
};
config = mkIf cfg.enable {
assertions = mapAttrsToList (name: user: {
assertion = xor (user.hashedPassword != null) (user.passwordFile != null);
message = ''
Exactly one of `services.ntfy-sh.accessControl.users.<name>.hashedPassword`
and `services.ntfy-sh.accessControl.users.<name>.passwordFile`
is required for `name` = `${name}`.
'';
}) cfg.users;
services.ntfy-sh.settings = {
enable-signup = false;
};
systemd.tmpfiles.rules = [
"f /var/lib/ntfy-sh/.acl-path 0600 ${config.services.ntfy-sh.user} ${config.services.ntfy-sh.group} - -"
];
systemd.services.ntfy-sh.preStart = "${ntfy-acl}/bin/ntfy-acl";
};
}

View file

@ -0,0 +1,86 @@
#!@python3@/bin/python
import json
import sqlite3
import subprocess
def ntfy(*args: str, env=None):
subprocess.run(["ntfy"] + list(args), env=env).check_returncode()
def create_user(u: str, role: str, passwordFile: str, hashedPassword: str):
# Create the user with the required role and password
if passwordFile != None:
with open(passwordFile) as pwd_fp:
env = {"NTFY_PASSWORD": pwd_fp.read().strip()}
ntfy("user", "add", f"--role={role}", u, env=env)
else:
env = {"NTFY_PASSWORD": hashedPassword}
ntfy("user", "add", f"--role={role}", u, env=env)
# HACK: add does not supports hashedPassword entry
ntfy("user", "change-pass-hash", u, env=env)
def update_user(u: str, role: str, passwordFile: str, hashedPassword: str):
# Update the user with the required role and password
if passwordFile != None:
with open(passwordFile) as pwd_fp:
env = {"NTFY_PASSWORD": pwd_fp.read().strip()}
ntfy("user", "change-pass", u, env=env)
else:
env = {"NTFY_PASSWORD": hashedPassword}
ntfy("user", "change-pass-hash", u, env=env)
ntfy("user", "change-role", u, role)
# Compare the ACL file path to the one used to get the actual data
try:
with open("/var/lib/ntfy-sh/.acl-path") as acl_path_fp:
acl_path: str = acl_path_fp.read().strip()
except OSError:
print("[!] Cannot open .acl-path")
exit(1)
if acl_path == "@acl_file@":
print("[-] Unchanged ACL file, exiting")
exit(0)
else:
print("[+] ACL file has changed, updating data")
# Get the wanted state
with open("@acl_file@") as acl_fp:
acl_data = json.load(acl_fp)
# Connect to the db to recover the list of current users
with sqlite3.connect("@user_db@") as con:
c = con.cursor()
existing_users: set[str] = set(c.execute("SELECT user FROM user")) - {"*"}
wanted_users: set[str] = set(acl_data["users"].keys())
# Delete extraneous users
for user in existing_users - wanted_users:
ntfy("user", "del", user)
# Create new users
for user in wanted_users - existing_users:
create_user(user, **acl_data["users"][user])
# Update existing users
for user in existing_users & wanted_users:
update_user(user, **acl_data["users"][user])
# Reset ACL rules
ntfy("access", "--reset")
for rule in acl_data["access"]:
ntfy("access", rule["user"], rule["topic"], rule["permission"])
# Write the new ACL file path
with open("/var/lib/ntfy-sh/.acl-path", "w") as f:
f.write("@acl_file@")