selfprivacy-nixos-config/ldap/ldap-module.nix

364 lines
13 KiB
Nix
Raw Normal View History

{ config, pkgs, lib, ... }:
let
cfg = config.selfprivacy.ldap;
domain = lib.concatMapStringsSep "," (x: "dc=${x}") (lib.splitString "." cfg.domain);
openssh-ldap-publickey = pkgs.fetchFromGitHub {
owner = "AndriiGrytsenko";
repo = "openssh-ldap-publickey";
rev = "v1.0.2";
hash = "sha256-Citukp6dQrmFUGFTRSXAhoUpjKUlEvkAOffx2/P5Gag=";
};
in
{
options = {
selfprivacy.ldap = {
enable = lib.mkEnableOption (lib.mdDoc "LDAP integration");
domain = lib.mkOption {
type = lib.types.str;
example = "example.com";
description = ''
LDAP domain.
'';
};
rootUser = lib.mkOption {
type = lib.types.str;
default = "root";
description = lib.mdDoc ''
LDAP root user.
'';
};
rootHashedPassword = lib.mkOption {
type = lib.types.passwdEntry lib.types.str;
description = lib.mdDoc ''
LDAP root user hashed password.
'';
};
users = lib.mkOption {
type = lib.types.listOf (lib.types.submodule {
options = {
username = lib.mkOption {
type = lib.types.passwdEntry lib.types.str;
example = "john";
description = lib.mdDoc ''
User's username.
'';
};
hashedPassword = lib.mkOption {
type = lib.types.passwdEntry lib.types.str;
description = lib.mdDoc ''
Specifies the hashed password for the user.
'';
};
sshKeys = lib.mkOption {
type = lib.types.listOf lib.types.singleLineStr;
default = [ ];
description = lib.mdDoc ''
A list of user's OpenSSH public keys.
'';
};
email = lib.mkOption {
type = lib.types.str;
example = "john@example.com";
description = lib.mdDoc ''
User email for LDAP.
'';
};
displayName = lib.mkOption {
type = lib.types.str;
example = "John Doe";
default = "";
description = lib.mdDoc ''
Display name for LDAP.
'';
};
firstName = lib.mkOption {
type = lib.types.str;
example = "John";
default = "";
description = lib.mdDoc ''
User's first name for LDAP.
'';
};
lastName = lib.mkOption {
type = lib.types.str;
example = "Doe";
default = "";
description = lib.mdDoc ''
User's last name for LDAP.
'';
};
jpegPhoto = lib.mkOption {
type = lib.types.nullOr lib.types.singleLineStr;
default = null;
description = lib.mdDoc ''
A jpegPhoto attribute for LDAP, base64-encoded.
'';
};
groups = lib.mkOption {
type = lib.types.listOf (lib.types.enum [
"admin"
"gitea"
"nextcloud"
"pleroma"
2023-03-28 18:33:44 +00:00
"mastodon"
]);
example = [ "gitea" ];
default = [ ];
description = lib.mdDoc ''
Which services the user is allowed to use.
'';
};
};
});
default = [ ];
description = lib.mdDoc ''
List of LDAP users.
'';
};
};
};
config = lib.mkMerge [
(lib.mkIf cfg.enable {
services.openldap =
let
filterUsers = group: users: lib.filter
(user: builtins.elem group user.groups)
users;
mkUser = usersNamespace: user: lib.concatStringsSep "\n" ([
"dn: uid=${user.username},ou=${usersNamespace},${domain}"
"objectClass: inetOrgPerson"
"objectClass: shadowAccount"
] ++ lib.optionals (user.sshKeys != [ ]) [
"objectClass: ldapPublicKey"
] ++ lib.optionals (user.jpegPhoto != null) [
"jpegPhoto:: ${user.jpegPhoto}"
] ++ (map (key: "sshPublicKey: ${key}") user.sshKeys)
++ [
"mail: ${user.email}"
"displayName: ${user.displayName}"
"cn: ${user.firstName}"
"sn: ${user.lastName}"
"userPassword: {crypt}${user.hashedPassword}"
]);
mkGroup = usersNamespace: users: group:
let
groupUsers = (filterUsers group users);
in
lib.optionalString (groupUsers != [ ]) ''
dn: cn=${group},ou=groups,${domain}
objectClass: groupOfNames
${lib.concatMapStringsSep
"\n"
(user: "member: uid=${user.username},ou=${usersNamespace},${domain}")
groupUsers
}
'';
mkUsersNamespace = usersNamespace: users: ''
dn: ou=${usersNamespace},${domain}
objectClass: organizationalUnit
${lib.concatMapStringsSep "\n\n" (mkUser usersNamespace) users}
'';
mkGroupsNamespace = usersNamespace: users: groupsNamespace: groups: ''
dn: ou=${groupsNamespace},${domain}
objectClass: organizationalUnit
${lib.concatMapStringsSep "\n\n" (mkGroup usersNamespace users) groups}
'';
in
{
enable = true;
urlList = [ "ldap://localhost:389" ];
declarativeContents."${domain}" = ''
dn: ${domain}
objectClass: domain
${mkUsersNamespace "users" cfg.users}
2023-03-28 18:33:44 +00:00
# Make a root user for some services to bind
dn: uid=root,ou=users,${domain}
objectClass: inetOrgPerson
cn: root
sn: root
mail: root@${domain}
# Password is "root"
userPassword: {crypt}$6$teiD8ySLE58taSvY$veZS9QRSmfBcox2JfgYH/AWv24cpHD4P7IUzFv8WgxUaio.j7Y4aqMcC4a17v3PvOdCu8vgkKAtu/jhhKjVQm0
${mkGroupsNamespace "users" cfg.users "groups" [
"admin"
"gitea"
"nextcloud"
2023-03-28 18:33:44 +00:00
"mastodon"
]}
# pleroma has no support for ldap filters
# so we just put pleroma users under separate namespace
# https://git.pleroma.social/pleroma/pleroma/-/issues/1645
${mkUsersNamespace "pleroma" (filterUsers "pleroma" cfg.users)}
'';
settings = {
children = {
"cn=schema".includes = [
"${pkgs.openldap}/etc/schema/core.ldif"
"${pkgs.openldap}/etc/schema/cosine.ldif"
"${pkgs.openldap}/etc/schema/dyngroup.ldif"
"${pkgs.openldap}/etc/schema/inetorgperson.ldif"
"${pkgs.openldap}/etc/schema/nis.ldif"
"${openssh-ldap-publickey}/misc/openssh-lpk-openldap.ldif"
];
"cn=modules" = {
attrs = {
objectClass = [ "olcModuleList" ];
olcModuleLoad = "dynlist";
};
};
"olcDatabase={0}config" = {
attrs = {
objectClass = [ "olcDatabaseConfig" ];
olcDatabase = "{0}config";
olcRootDN = "cn=${cfg.rootUser},cn=config";
olcRootPW = "{crypt}${cfg.rootHashedPassword}";
};
};
"olcDatabase={1}mdb" = {
attrs = {
objectClass = [ "olcDatabaseConfig" "olcMdbConfig" ];
olcDatabase = "{1}mdb";
olcDbDirectory = "/var/lib/openldap/db";
olcSuffix = "${domain}";
olcRootDN = "cn=${cfg.rootUser},${domain}";
olcRootPW = "{crypt}${cfg.rootHashedPassword}";
};
};
"olcOverlay=dynlist,olcDatabase={1}mdb" = {
attrs = {
objectClass = [ "olcOverlayConfig" "olcDynListConfig" ];
olcDynListAttrSet = "groupOfURLs memberURL member+memberOf@groupOfNames";
};
};
};
};
};
})
(lib.mkIf config.services.gitea.enable {
systemd.services.gitea.preStart = lib.mkAfter ''
ldap_id=$(${config.services.gitea.package}/bin/gitea admin auth list | grep nixos-ldap | ${pkgs.gawk}/bin/awk '{ print $1 }' || true)
${lib.optionalString (!cfg.enable) ''
if [ ! -z "$ldap_id" ]; then
${config.services.gitea.package}/bin/gitea admin auth update-ldap \
--id $ldap_id \
--not-active
fi
''}
${lib.optionalString cfg.enable ''
if [ -z "$ldap_id" ]; then
auth_command="add-ldap"
else
auth_command="update-ldap --id $ldap_id"
fi
# https://docs.gitea.io/en-us/command-line/#admin
${config.services.gitea.package}/bin/gitea admin auth $auth_command \
--name nixos-ldap \
--security-protocol unencrypted \
--host 127.0.0.1 \
--port 389 \
--bind-dn "${domain}" \
--user-search-base "ou=users,${domain}" \
--user-filter "(&(objectClass=shadowAccount)(memberOf=cn=gitea,ou=groups,${domain})(uid=%s))" \
--admin-filter "(&(objectClass=shadowAccount)(memberOf=cn=admin,ou=groups,${domain}))" \
--username-attribute uid \
--email-attribute mail \
--firstname-attribute cn \
--surname-attribute sn \
--avatar-attribute jpegPhoto \
--public-ssh-key-attribute sshPublicKey \
--synchronize-users
''}
'';
})
(lib.mkIf config.services.nextcloud.enable {
# No support for admins via LDAP yet:
# https://github.com/nextcloud/server/issues/6428
systemd.services.nextcloud-setup.script =
let
# https://docs.nextcloud.com/server/25/admin_manual/configuration_server/occ_command.html#ldap-commands
# https://docs.nextcloud.com/server/25/admin_manual/configuration_user/user_auth_ldap_api.html#configuration-keys
occAction = action: "nextcloud-occ --no-interaction ${action}";
ldapAction = action: occAction "ldap:${action}";
ldapConfigIdFile = lib.escapeShellArg "${config.services.nextcloud.datadir}/config/.ldap-nixos-config-id";
ldapConfigAction = action: "${ldapAction action} $(<${ldapConfigIdFile})";
ldapSetConfig = ldapConfigAction "set-config";
ldapTestConfig = ldapConfigAction "test-config";
in
lib.mkAfter ''
${lib.optionalString (!cfg.enable) ''
if [ -f ${ldapConfigIdFile} ]; then
${ldapSetConfig} ldapConfigurationActive 0
fi
''}
${lib.optionalString cfg.enable ''
if [ ! -f ${ldapConfigIdFile} ]; then
${occAction "app:enable"} user_ldap
if ! ${ldapAction "create-empty-config"} --only-print-prefix > ${ldapConfigIdFile}; then
rm ${ldapConfigIdFile}
echo "Failed to create LDAP configuration"
exit 1
fi
fi
${ldapSetConfig} ldapHost 127.0.0.1
${ldapSetConfig} ldapPort 389
${ldapSetConfig} ldapBase "${domain}"
${ldapSetConfig} ldapBaseUsers "ou=users,${domain}"
${ldapSetConfig} ldapUserFilter "(&(objectClass=shadowAccount)(memberOf=cn=nextcloud,ou=groups,${domain}))"
${ldapSetConfig} ldapLoginFilter "(&(objectClass=shadowAccount)(memberOf=cn=nextcloud,ou=groups,${domain})(uid=%uid))"
${ldapSetConfig} ldapExpertUsernameAttr uid
${ldapSetConfig} ldapEmailAttribute mail
${ldapSetConfig} ldapUserDisplayName displayName
${ldapSetConfig} ldapUserAvatarRule "data:jpegPhoto"
if ${ldapTestConfig} | grep -q 'configuration is valid and the connection could be established'; then
${ldapSetConfig} ldapConfigurationActive 1
else
echo "LDAP configuration is invalid, disabling"
${ldapSetConfig} ldapConfigurationActive 0
fi
''}
'';
})
2023-03-28 18:33:44 +00:00
(lib.mkIf (config.services.mastodon.enable && cfg.enable) {
services.mastodon.extraConfig = {
LDAP_ENABLED = true;
LDAP_HOST = "127.0.0.1";
LDAP_PORT = 389;
LDAP_BASE = "ou=users,${domain}";
LDAP_BIND_DN = "uid=root,ou=users,${domain}";
LDAP_BIND_PASSWORD = "root";
LDAP_UID = "uid";
LDAP_MAIL = "mail";
LDAP_SEARCH_FILTER = "(&(objectClass=shadowAccount)(memberOf=cn=mastodon,ou=groups,${domain})(uid=%{username}))";
};
})
(lib.mkIf (config.services.pleroma.enable && cfg.enable) {
services.pleroma.configs = [
''
import Config
config :pleroma, Pleroma.Web.Auth.Authenticator, Pleroma.Web.Auth.LDAPAuthenticator
config :pleroma, :ldap,
enabled: true,
host: "localhost",
port: 389,
ssl: false,
base: "ou=pleroma,${domain}",
uid: "uid"
''
];
})
];
}