From 82578e35318b4d5a5f206d1dd5016f3c176875fb Mon Sep 17 00:00:00 2001 From: Inex Code Date: Tue, 28 Mar 2023 19:31:02 +0300 Subject: [PATCH] feat(ldap): Add LDAP support Co-authored-by: misuzu --- configuration.nix | 1 + git/gitea.nix | 10 +- ldap/ldap-module.nix | 339 +++++++++++++++++++++++++++++++++++++++++++ users.nix | 16 ++ 4 files changed, 361 insertions(+), 5 deletions(-) create mode 100644 ldap/ldap-module.nix diff --git a/configuration.nix b/configuration.nix index 513f854..0307d73 100644 --- a/configuration.nix +++ b/configuration.nix @@ -10,6 +10,7 @@ in ./variables.nix ./files.nix ./volumes.nix + ./ldap/ldap-module.nix ./users.nix ./mailserver/system/mailserver.nix ./vpn/ocserv.nix diff --git a/git/gitea.nix b/git/gitea.nix index 29d3f1e..389a01b 100644 --- a/git/gitea.nix +++ b/git/gitea.nix @@ -13,10 +13,10 @@ in gitea = { enable = cfg.gitea.enable; stateDir = "/var/lib/gitea"; -# log = { -# rootPath = "/var/lib/gitea/log"; -# level = "Warn"; -# }; + # log = { + # rootPath = "/var/lib/gitea/log"; + # level = "Warn"; + # }; user = "gitea"; database = { type = "sqlite3"; @@ -40,7 +40,7 @@ in rootUrl = "https://git.${cfg.domain}/"; httpAddress = "0.0.0.0"; httpPort = 3000; -# cookieSecure = true; + # cookieSecure = true; settings = { mailer = { ENABLED = false; diff --git a/ldap/ldap-module.nix b/ldap/ldap-module.nix new file mode 100644 index 0000000..42d3bb8 --- /dev/null +++ b/ldap/ldap-module.nix @@ -0,0 +1,339 @@ +{ 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" + ]); + 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} + + ${mkGroupsNamespace "users" cfg.users "groups" [ + "admin" + "gitea" + "nextcloud" + ]} + + # 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 \ + --id $ldap_id \ + --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 + ''} + ''; + }) + (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" + '' + ]; + }) + ]; +} diff --git a/users.nix b/users.nix index 285d89b..f4ad156 100644 --- a/users.nix +++ b/users.nix @@ -22,4 +22,20 @@ in }) cfg.users); }; + selfprivacy.ldap = { + enable = true; + domain = "${cfg.domain}"; + rootUser = "${cfg.username}"; + rootHashedPassword = cfg.hashedMasterPassword; + users = [ + (builtins.map + (user: { + username = "${user.username}"; + email = "${user.username}@${cfg.domain}"; + hashedPassword = user.hashedPassword; + groups = [ "gitea" "nextcloud" "pleroma" ]; + }) + cfg.users) + ]; + }; }