Compare commits

...

85 Commits

Author SHA1 Message Date
536f11bbfd dovwiefiwuef' 2025-09-26 16:37:55 +03:00
271f3289e5 fix 2025-09-26 16:06:15 +03:00
ac56849a3b fix 2025-09-26 15:54:02 +03:00
0050a4130b fix 2025-09-25 23:37:30 +03:00
828d5853d6 fix 2025-09-25 23:35:22 +03:00
197d699fda fix 2025-09-25 23:33:22 +03:00
ce10a8595d fix 2025-09-25 23:21:25 +03:00
94eea2d425 fix 2025-09-25 22:59:59 +03:00
3eb40713b1 fix 2025-09-25 22:37:14 +03:00
a1183aab56 fix 2025-09-25 17:23:58 +03:00
de7e645f11 fix 2025-09-25 17:14:45 +03:00
67d2c839a5 fix 2025-09-24 17:52:44 +03:00
0f773262a1 fix 2025-09-24 17:15:33 +03:00
30dcbcb407 fix 2025-09-24 17:10:57 +03:00
98832beb27 fix 2025-09-24 17:05:07 +03:00
4008a92a49 fix 2025-09-24 15:59:36 +03:00
2b6d82229c fix 2025-09-24 15:47:39 +03:00
d0c6d44528 fix 2025-09-23 23:40:12 +03:00
e429b56384 fix 2025-09-23 23:35:58 +03:00
5b174702d8 fix 2025-09-23 23:34:17 +03:00
703fcaec39 fix 2025-09-23 23:21:04 +03:00
69a4634f8a fix 2025-09-23 23:14:48 +03:00
e349c5e0f3 fix 2025-09-23 23:11:25 +03:00
48b47cfa88 fix 2025-09-23 23:07:05 +03:00
660dd1854f fix 2025-09-23 22:54:10 +03:00
69692faac5 fix 2025-09-23 22:49:51 +03:00
95153cd55b fix 2025-09-23 22:46:49 +03:00
9f3c42e3f9 fix 2025-09-23 17:20:13 +03:00
67f9dc73e5 fix 2025-09-23 17:14:11 +03:00
58b4bc47a6 fix 2025-09-23 16:29:00 +03:00
7e62a9ff70 fix 2025-09-23 16:15:21 +03:00
ff4e5a634c fix 2025-09-23 16:14:53 +03:00
79710d5afa fix 2025-09-23 16:09:49 +03:00
97852db654 fix 2025-09-23 16:00:23 +03:00
08cca48255 fix 2025-09-23 15:53:51 +03:00
3294ff3da4 fix 2025-09-20 12:46:40 +03:00
dcc73050eb fix 2025-09-20 02:29:39 +03:00
a1a3aaa9d3 fix 2025-09-20 01:07:34 +03:00
26e81611be fix 2025-09-20 00:53:43 +03:00
1235104b10 fix 2025-09-20 00:09:46 +03:00
31c035ecf1 fix(email-options): fix 2025-09-18 19:13:59 +03:00
4942e7c359 fix(email-options): 2025-09-18 18:46:34 +03:00
2826305218 improvement 2025-09-18 18:11:26 +03:00
7b317fbce3 fix 2025-09-17 17:22:10 +03:00
f6f58f17ca fix 2025-09-17 16:18:20 +03:00
e796c9f657 fix 2025-09-16 19:15:35 +03:00
71b96ada37 fix 2025-09-16 18:21:51 +03:00
a29dc78eea fix 2025-09-16 18:04:08 +03:00
9b352ec85a fix 2025-09-16 17:12:44 +03:00
727c01f9d8 temp 2025-09-16 16:50:34 +03:00
dddbf8b35f test 2025-09-13 16:29:17 +03:00
a61b71c7c8 fix 2025-09-13 12:47:02 +03:00
3c0030b80b fix 2025-09-13 12:32:13 +03:00
c4a780b7ff fix 2025-09-13 11:34:54 +03:00
d5761ef11b fix 2025-09-13 10:31:28 +03:00
77bdedef79 fix 2025-09-13 03:06:36 +03:00
e3b6a5243f fix 2025-09-13 02:59:37 +03:00
68337eba4e fix 2025-09-13 02:47:42 +03:00
ab04e2ada2 fix 2025-09-13 02:38:29 +03:00
d90e14a66d fix 2025-09-13 01:39:15 +03:00
2d01f52342 fix 2025-09-13 01:13:21 +03:00
23b7957317 fix 2025-09-13 00:42:54 +03:00
88b88dba4b fix 2025-09-13 00:26:02 +03:00
bb52306cf4 test 2025-09-13 00:20:53 +03:00
308c661f90 test 2025-09-12 23:36:48 +03:00
8186824c60 test 2025-09-12 21:57:38 +03:00
8b5a67bdc1 test 2025-09-12 20:58:32 +03:00
fcf4163154 test 2025-09-12 20:29:34 +03:00
88269b9e74 test 2025-09-12 19:46:34 +03:00
22d8579f67 test 2025-09-12 19:22:27 +03:00
2397cd1090 test 2025-09-12 19:16:33 +03:00
4c05645a50 test 2025-09-12 19:03:11 +03:00
bbb267eafd debug 2025-09-12 18:53:41 +03:00
43c7b76c7a test 2025-09-12 18:20:57 +03:00
249e6bc9d4 test 2025-09-12 18:12:53 +03:00
a1424478ba test 2025-09-12 18:05:17 +03:00
d89f84fd5b test 2025-09-12 17:52:24 +03:00
e6db25d6e9 fix 2025-09-12 16:46:24 +03:00
0c0b5bb29a fix 2025-09-12 16:45:49 +03:00
8671a28baf fix 2025-09-12 15:40:59 +03:00
c34269a83e fix 2025-09-12 15:26:28 +03:00
68fd025c02 fix 2025-09-12 15:08:34 +03:00
25133ecf7c fix 2025-09-12 15:05:42 +03:00
b8e6847217 fix 2025-09-12 15:03:34 +03:00
e5b10038b2 fix 2025-09-12 15:02:04 +03:00
4 changed files with 257 additions and 52 deletions

View File

@@ -1,8 +1,9 @@
[
[ "selfprivacy", "domain" ],
[ "selfprivacy", "username" ],
[ "selfprivacy", "modules", "auth", "enable" ],
[ "selfprivacy", "modules", "mastodon" ],
[ "selfprivacy", "passthru", "auth", "mkOAuth2ClientSecretFP" ],
[ "selfprivacy", "passthru", "auth" ],
[ "selfprivacy", "passthru", "auth", "oauth2-discovery-url" ],
[ "selfprivacy", "passthru", "auth", "oauth2-provider-name" ],
[ "selfprivacy", "sso", "enable" ],
@@ -11,6 +12,8 @@
[ "services", "mastodon", "package" ],
[ "services", "mastodon", "user" ],
[ "services", "mastodon", "group" ],
[ "services", "mastodon", "database" ],
[ "services", "postfix", "user" ],
[ "services", "postfix", "group" ]
[ "services", "postfix", "group" ],
[ "services", "kanidm", "serverSettings", "origin" ]
]

View File

@@ -1,6 +1,5 @@
{
# TODO: check whether there is no TODOs
# TODO: check whether there is no hedgegdoc mentions
description = "Mastodon module";
outputs = { ... }:
@@ -35,6 +34,7 @@
supportLevel = "normal";
sso = {
userGroup = "sp.mastodon.users";
adminsGroup = "sp.mastodon.admins";
};
};
};

148
mastodon-kanidm-sync.py Normal file
View File

@@ -0,0 +1,148 @@
import os
import json
import time
import requests
import psycopg2 as ps
def read_file(path):
with open(path, "r", encoding="utf-8") as f:
return f.read()
def getenv(name):
try:
return os.environ[name]
except KeyError:
print(f"[ERROR] Missing environment variable {name}. You should NOT run this script by hand, please use systemd mastodon-kanidm-sync.service.")
exit(1)
# Import configuration
KANIDM_URL = getenv("KANIDM_URL")
KANIDM_TOKEN = read_file(getenv("KANIDM_TOKEN_PATH")).strip()
OWNER_USERNAME = getenv("OWNER_USERNAME")
SLEEP_TIME = int(getenv("SLEEP_TIME"))
def sync_mastodon():
# Fetch kanidm users list from userdata file
# Userdata file is json list with information about what users are configured by kanidm
try:
USERDATA = read_file(getenv("USERDATA_FILE_PATH")).strip()
userdata = json.loads(USERDATA)
print("[INFO] ")
except FileNotFoundError:
userdata = []
# Load database
conn = ps.connect(
dbname=getenv("POSTGRES_DBNAME"),
user=getenv("POSTGRES_USER"),
host=getenv("POSTGRES_HOST")
)
# Fetch current userdata from database
cur = conn.cursor()
cur.execute('''
SELECT identities.uid, users.id, user_roles.name
FROM users
JOIN identities
ON users.id = identities.id
LEFT JOIN user_roles
ON users.role_id = user_roles.id;
'''
)
state = cur.fetchall()
users = {}
for i in state:
users[i[0]] = {
"id": i[1],
"role": i[2],
"isKanidmUser": False
}
# Fetch Kanidm userdata
kanidm_users_raw = requests.get(
f"{KANIDM_URL}/v1/person",
headers={
"Authorization": f"Bearer {KANIDM_TOKEN}",
"Content-Type": "application/json",
},
timeout=5,
).json()
def give_role(uid, role, putUserdata = True):
if (uid not in userdata) and (putUserdata):
userdata.append(uid)
users[uid]["isKanidmUser"] = True
users[uid]["role"] = role
print(f"[INFO] {uid} is marked as {role}")
for i in kanidm_users_raw:
i = i["attrs"]
for uid in i["name"]: # [user].attrs.name is a list
if uid in users: # Don't apply anything for users who have no mastodon access (sp.mastodon.users) or didn't register
if uid == OWNER_USERNAME:
give_role(uid, "Owner", False)
continue
for group in i["memberof"]:
if group.startswith("sp.mastodon.admins@") or group.startswith("sp.admins@"):
give_role(uid, "Admin")
break
elif group.startswith("sp.mastodon.moderators@"):
give_role(uid, "Moderator")
break
elif uid in userdata:
# If user, who previously had a role, has no roles set by Kanidm, delete them from userdata list so allow setting roles directly by mastodon
give_role(uid, None, False)
userdata.remove(uid)
print("[DEBUG]", users)
# Fetch RoleIDs
cur = conn.cursor()
cur.execute("SELECT id, name FROM user_roles;")
roles_raw = cur.fetchall()
roles = {}
for i in roles_raw:
roles[i[1]] = i[0]
# Give roles
for uid in users:
if not users[uid]["isKanidmUser"]:
continue
if users[uid]["role"]:
rolename = users[uid]["role"]
roleid = roles[rolename]
else:
roleid = "NULL"
sqlcommand = f"UPDATE users SET role_id = {roleid} WHERE id = {users[uid]["id"]};"
print("[DEBUG] SQL:", sqlcommand)
cur.execute(sqlcommand)
conn.commit()
cur.close()
conn.close()
print("[INFO] Final userdata.json file content: ", userdata)
def write_userdata(mode):
with open(getenv("USERDATA_FILE_PATH"), mode) as f:
f.write(json.dumps(userdata))
f.close()
try:
write_userdata("w")
except FileNotFoundError:
print("[INFO] userdata.json file doesn't exist. Creating it")
write_userdata("x")
while True:
sync_mastodon()
time.sleep(SLEEP_TIME)

View File

@@ -9,17 +9,21 @@ let
cfg = sp.modules.mastodon;
oauthClientID = "mastodon";
auth-passthru = config.selfprivacy.passthru.auth;
oauthDiscoveryURL = config.services.kanidm.serverSettings.origin;
oauthDiscoveryURL = auth-passthru.oauth2-discovery-url oauthClientID;
issuer = lib.strings.removeSuffix "/.well-known/openid-configuration" oauthDiscoveryURL;
oauthRedirectURL = "https://${cfg.subdomain}.${sp.domain}/auth/auth/openid_connect/callback";
usersGroup = "sp.mastodon.users";
adminsGroup = "sp.mastodon.admins";
oauthClientSecretFP = auth-passthru.mkOAuth2ClientSecretFP oauthClientID;
oauthRedirectURL = "https://${cfg.subdomain}.${sp.domain}/auth/auth/openid_connect/callback";
serviceAccountFP = auth-passthru.mkServiceAccountTokenFP "mastodon";
# emailPassword = pkgs.runCommand "genpassword" {} "echo `head -c 32 /dev/urandom | base64 | sed 's/[+=\\/A-Z]//g'` > $out";
# emailPasswordHash = pkgs.runCommand "genpassword" {} "echo `head -c 32 /dev/urandom | base64 | sed 's/[+=\\/A-Z]//g'` > $out";
secrets = rec {
dir = "/run/keys/mastodon";
hashedPasswordFile = "${dir}/hashed_email_password";
passwordFile = "${dir}/email_password";
};
in
{
options.selfprivacy.modules.mastodon = {
@@ -48,7 +52,7 @@ in
(lib.mkOption {
default = "mastodon";
type = lib.types.strMatching "[A-Za-z0-9][A-Za-z0-9\-]{0,61}[A-Za-z0-9]";
description = "Subdomain";
description = "Subdomain (changing subdomain after setting up will cause breakage of the server!)";
})
// {
meta = {
@@ -58,16 +62,22 @@ in
weight = 0;
};
};
dissallowUnauthenticatedAPI =
(lib.mkOption {
default = true;
type = lib.types.bool;
description = "Allow unauthenticated API access";
})
// {
meta = {
type = "bool";
weight = 1;
};
};
};
config = lib.mkIf cfg.enable {
assertions = [
{
assertion = sp.sso.enable;
message = "Mastodon cannot be enabled when SSO is disabled.";
}
];
fileSystems = lib.mkIf sp.useBinds {
"/var/lib/mastodon" = {
device = "/volumes/${cfg.location}/mastodon";
@@ -75,83 +85,123 @@ in
};
};
# services.postgresql = {
# ensureDatabases = [ "mastodon" ];
# ensureUsers = [
# {
# name = "mastodon";
# ensureDBOwnership = true;
# }
# ];
# };
services.mastodon = {
enable = true;
localDomain = "${cfg.subdomain}.${sp.domain}";
enableUnixSocket = false;
configureNginx = true;
database.createLocally = true;
streamingProcesses = 3;
streamingProcesses = 2;
smtp = {
createLocally = false;
user = "noreply.mastodon@${sp.domain}";
fromAddress = "noreply.mastodon@${sp.domain}";
passwordFile = "/run/keys/mastodon/email_password";
user = "noreply.mastodon";
passwordFile = secrets.passwordFile;
authenticate = true;
host = "hollowness.top";
port = 465;
};
extraConfig = {
"SMTP_ENABLE_STARTTLS_AUTO" = "true"; # Simple NixOS MailServer doesn't allow connections without SSL
"SMTP_ENABLE_STARTTLS" = "always";
"SMTP_TLS" = "true";
"SMTP_SSL" = "true";
"DISALLOW_UNAUTHENTICATED_API_ACCESS" = lib.boolToString cfg.dissallowUnauthenticatedAPI;
};
};
mailserver.loginAccounts."noreply.mastodon@${sp.domain}" = {
hashedPassword = "/run/keys/mastodon/email_password";
selfprivacy.emails."noreplymastodon" = {
hashedPasswordFile = secrets.hashedPasswordFile;
systemdTargets = [ "mastodon-email-password-setup.service" ];
sendOnly = true;
};
services.postfix.config.virtual_mailbox_maps = [ "hash:/run/postfix/mastodon.cf" ];
systemd = {
services.mastodon-email-password-setup = {
enable = true;
wantedBy = [ "multi-user.target" "mastodon-web.service" "postfix.service" ];
serviceConfig = {
Slice = "mastodon.slice";
Type = "oneshot";
ExecStart = pkgs.writeShellScript "gen-mastodon-email-password" ''
export password=$(head -c 32 /dev/urandom | base64 | sed 's/[+=\\/A-Z]//g')
mkdir ${secrets.dir} || true # Create ${secrets.dir} if it doesn't exist
rm -f /run/keys/mastodon/email_password || true
mkdir /run/keys/mastodon/ || true # Create /run/keys/mastodon if it doesn't exist
echo $password > /run/keys/mastodon/email_password
chmod 400 /run/keys/mastodon/email_password
chown ${config.services.mastodon.user}:${config.services.mastodon.group} /run/keys/mastodon/email_password
rm -f ${secrets.passwordFile} || true
echo "$password" > ${secrets.passwordFile}
chmod 400 ${secrets.passwordFile}
chown ${config.services.mastodon.user}:${config.services.mastodon.group} ${secrets.passwordFile}
rm -f /run/postfix/mastodon.cf || true
mkdir /run/postfix/ || true # Create /run/postfix if it doesn't exist
export hashedPassword=$(mkpasswd -sm bcrypt "$password")
echo "noreply.mastodon@${sp.domain}: $hashedPassword" > /run/postfix/mastodon.cf
chmod 440 /run/postfix/mastodon.cf
chown ${config.services.postfix.user}:${config.services.postfix.group} /run/postfix/mastodon.cf
export hashedPassword=$(${lib.getExe pkgs.mkpasswd} -sm bcrypt "$password")
rm -f ${secrets.hashedPasswordFile} || true
echo "$hashedPassword" > ${secrets.hashedPasswordFile}
chmod 440 ${secrets.hashedPasswordFile}
chown ${config.services.postfix.user}:${config.services.postfix.group} ${secrets.hashedPasswordFile}
'';
};
};
services.mastodon-kanidm-sync = {
after = [
"postgresql.service"
"kanidm.service"
];
requires = [
"kanidm.service"
"postgresql.service"
];
wantedBy = [ "multi-user.target" ];
environment = let db = config.services.mastodon.database;
in {
KANIDM_URL = config.services.kanidm.serverSettings.origin;
KANIDM_TOKEN_PATH = serviceAccountFP;
POSTGRES_DBNAME = db.name;
POSTGRES_USER = db.user;
POSTGRES_HOST = db.host;
USERDATA_FILE_PATH = "/var/lib/mastodon/.userdata.json";
OWNER_USERNAME = sp.username;
SLEEP_TIME = "30";
};
serviceConfig = {
Slice = "mastodon.slice";
User = "mastodon";
Group = "mastodon";
LoadCredential = [ "kanidm-token:${serviceAccountFP}" ];
ExecStart = pkgs.writers.writePython3 "mas-kanidm-sync" {
doCheck = false;
libraries = with pkgs.python3Packages; [
requests
psycopg2
];
} (builtins.readFile ./mastodon-kanidm-sync.py);
};
};
services.mastodon-web = {
unitConfig.RequiresMountsFor = lib.mkIf sp.useBinds "/volumes/${cfg.location}/mastodon";
serviceConfig = {
loadCredentials = ["client-secret:${oauthClientSecretFP}"];
ExecStart = lib.mkForce ''
export CLIENT_SECRET=$(cat $CREDENTIALS_DIRECTORY/client-secret)
${config.services.mastodon.package}/bin/puma -C config/puma.rb`
'';
Slice = "mastodon.slice";
LoadCredential = ["client-secret:${oauthClientSecretFP}"];
ExecStart = lib.mkForce (pkgs.writeShellScript "run-mastodon-with-client-secret" ''
export OIDC_CLIENT_SECRET=$(cat $CREDENTIALS_DIRECTORY/client-secret)
${config.services.mastodon.package}/bin/puma -C config/puma.rb
'');
};
environment = {
OIDC_ENABLED = "true";
OIDC_DISPLAY_NAME= "Kanidm";
OIDC_ISSUER = issuer;
OIDC_DISCOVERY = "true";
OIDC_SCOPE = "openid,profile";
OIDC_UID_FIELD = "sub";
OIDC_SCOPE = "openid,profile,email";
OIDC_UID_FIELD = "preferred_username";
OIDC_CLIENT_ID = oauthClientID;
OIDC_REDIRECT_URI = oauthRedirectURL;
OIDC_SECURITY_ASSUME_EMAIL_IS_VERIFIED = "false";
};
};
slices.mastodon = {
@@ -159,14 +209,18 @@ in
};
};
services.kanidm.provision.groups."sp.mastodon.moderators" = {};
selfprivacy.auth.clients.${oauthClientID} = {
inherit usersGroup;
inherit adminsGroup;
isTokenNeeded = true;
subdomain = cfg.subdomain;
originLanding = "https://${cfg.subdomain}.${sp.domain}/";
originUrl = oauthRedirectURL;
clientSystemdUnits = [ "mastodon.service" ];
enablePkce = true;
enablePkce = false;
useShortPreferredUsername = true;
linuxUserOfClient = "mastodon";
linuxGroupOfClient = "mastodon";
};