diff --git a/nixos/modules/config/mysql.nix b/nixos/modules/config/mysql.nix new file mode 100644 index 000000000000..8e7ce2a307e5 --- /dev/null +++ b/nixos/modules/config/mysql.nix @@ -0,0 +1,519 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + cfg = config.users.mysql; +in +{ + options = { + users.mysql = { + enable = mkEnableOption "Authentication against a MySQL/MariaDB database"; + host = mkOption { + type = types.str; + example = "localhost"; + description = "The hostname of the MySQL/MariaDB server"; + }; + database = mkOption { + type = types.str; + example = "auth"; + description = "The name of the database containing the users"; + }; + user = mkOption { + type = types.str; + example = "nss-user"; + description = "The username to use when connecting to the database"; + }; + passwordFile = mkOption { + type = types.path; + example = "/run/secrets/mysql-auth-db-passwd"; + description = "The path to the file containing the password for the user"; + }; + pam = mkOption { + description = "Settings for pam_mysql"; + type = types.submodule { + options = { + table = mkOption { + type = types.str; + example = "users"; + description = "The name of table that maps unique login names to the passwords."; + }; + updateTable = mkOption { + type = types.nullOr types.str; + default = null; + example = "users_updates"; + description = '' + The name of the table used for password alteration. If not defined, the value + of the table option will be used instead. + ''; + }; + userColumn = mkOption { + type = types.str; + example = "username"; + description = "The name of the column that contains a unix login name."; + }; + passwordColumn = mkOption { + type = types.str; + example = "password"; + description = "The name of the column that contains a (encrypted) password string."; + }; + statusColumn = mkOption { + type = types.nullOr types.str; + default = null; + example = "status"; + description = '' + The name of the column or an SQL expression that indicates the status of + the user. The status is expressed by the combination of two bitfields + shown below: + + + + + bit 0 (0x01): + if flagged, pam_mysql deems the account to be expired and + returns PAM_ACCT_EXPIRED. That is, the account is supposed + to no longer be available. Note this doesn't mean that pam_mysql + rejects further authentication operations. + + + + + bit 1 (0x02): + if flagged, pam_mysql deems the authentication token + (password) to be expired and returns PAM_NEW_AUTHTOK_REQD. + This ends up requiring that the user enter a new password. + + + + ''; + }; + passwordCrypt = mkOption { + example = "2"; + type = types.enum [ + "0" "plain" + "1" "Y" + "2" "mysql" + "3" "md5" + "4" "sha1" + "5" "drupal7" + "6" "joomla15" + "7" "ssha" + "8" "sha512" + "9" "sha256" + ]; + description = '' + The method to encrypt the user's password: + + + + + 0 (or "plain"): + No encryption. Passwords are stored in plaintext. HIGHLY DISCOURAGED. + + + + + 1 (or "Y"): + Use crypt(3) function. + + + + + 2 (or "mysql"): + Use the MySQL PASSWORD() function. It is possible that the encryption function used + by pam_mysql is different from that of the MySQL server, as + pam_mysql uses the function defined in MySQL's C-client API + instead of using PASSWORD() SQL function in the query. + + + + + 3 (or "md5"): + Use plain hex MD5. + + + + + 4 (or "sha1"): + Use plain hex SHA1. + + + + + 5 (or "drupal7"): + Use Drupal7 salted passwords. + + + + + 6 (or "joomla15"): + Use Joomla15 salted passwords. + + + + + 7 (or "ssha"): + Use ssha hashed passwords. + + + + + 8 (or "sha512"): + Use sha512 hashed passwords. + + + + + 9 (or "sha256"): + Use sha256 hashed passwords. + + + + ''; + }; + cryptDefault = mkOption { + type = types.nullOr (types.enum [ "md5" "sha256" "sha512" "blowfish" ]); + default = null; + example = "blowfish"; + description = "The default encryption method to use for passwordCrypt = 1."; + }; + where = mkOption { + type = types.nullOr types.str; + default = null; + example = "host.name='web' AND user.active=1"; + description = "Additional criteria for the query."; + }; + verbose = mkOption { + type = types.bool; + default = false; + description = '' + If enabled, produces logs with detailed messages that describes what + pam_mysql is doing. May be useful for debugging. + ''; + }; + disconnectEveryOperation = mkOption { + type = types.bool; + default = false; + description = '' + By default, pam_mysql keeps the connection to the MySQL + database until the session is closed. If this option is set to true it + disconnects every time the PAM operation has finished. This option may + be useful in case the session lasts quite long. + ''; + }; + logging = { + enable = mkOption { + type = types.bool; + default = false; + description = "Enables logging of authentication attempts in the MySQL database."; + }; + table = mkOption { + type = types.str; + example = "logs"; + description = "The name of the table to which logs are written."; + }; + msgColumn = mkOption { + type = types.str; + example = "msg"; + description = '' + The name of the column in the log table to which the description + of the performed operation is stored. + ''; + }; + userColumn = mkOption { + type = types.str; + example = "user"; + description = '' + The name of the column in the log table to which the name of the + user being authenticated is stored. + ''; + }; + pidColumn = mkOption { + type = types.str; + example = "pid"; + description = '' + The name of the column in the log table to which the pid of the + process utilising the pam_mysql's authentication + service is stored. + ''; + }; + hostColumn = mkOption { + type = types.str; + example = "host"; + description = '' + The name of the column in the log table to which the name of the user + being authenticated is stored. + ''; + }; + rHostColumn = mkOption { + type = types.str; + example = "rhost"; + description = '' + The name of the column in the log table to which the name of the remote + host that initiates the session is stored. The value is supposed to be + set by the PAM-aware application with pam_set_item(PAM_RHOST) + . + ''; + }; + timeColumn = mkOption { + type = types.str; + example = "timestamp"; + description = '' + The name of the column in the log table to which the timestamp of the + log entry is stored. + ''; + }; + }; + }; + }; + }; + nss = mkOption { + description = '' + Settings for libnss-mysql. + + All examples are from the minimal example + of libnss-mysql, but they are modified with NixOS paths for bash. + ''; + type = types.submodule { + options = { + getpwnam = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT username,'x',uid,'5000','MySQL User', CONCAT('/home/',username),'/run/sw/current-system/bin/bash' \ + FROM users \ + WHERE username='%1$s' \ + LIMIT 1 + ''; + description = '' + SQL query for the getpwnam + syscall. + ''; + }; + getpwuid = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT username,'x',uid,'5000','MySQL User', CONCAT('/home/',username),'/run/sw/current-system/bin/bash' \ + FROM users \ + WHERE uid='%1$u' \ + LIMIT 1 + ''; + description = '' + SQL query for the getpwuid + syscall. + ''; + }; + getspnam = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT username,password,'1','0','99999','0','0','-1','0' \ + FROM users \ + WHERE username='%1$s' \ + LIMIT 1 + ''; + description = '' + SQL query for the getspnam + syscall. + ''; + }; + getpwent = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT username,'x',uid,'5000','MySQL User', CONCAT('/home/',username),'/run/sw/current-system/bin/bash' FROM users + ''; + description = '' + SQL query for the getpwent + syscall. + ''; + }; + getspent = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT username,password,'1','0','99999','0','0','-1','0' FROM users + ''; + description = '' + SQL query for the getspent + syscall. + ''; + }; + getgrnam = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT name,password,gid FROM groups WHERE name='%1$s' LIMIT 1 + ''; + description = '' + SQL query for the getgrnam + syscall. + ''; + }; + getgrgid = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT name,password,gid FROM groups WHERE gid='%1$u' LIMIT 1 + ''; + description = '' + SQL query for the getgrgid + syscall. + ''; + }; + getgrent = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT name,password,gid FROM groups + ''; + description = '' + SQL query for the getgrent + syscall. + ''; + }; + memsbygid = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT username FROM grouplist WHERE gid='%1$u' + ''; + description = '' + SQL query for the memsbygid + syscall. + ''; + }; + gidsbymem = mkOption { + type = types.nullOr types.str; + default = null; + example = literalExpression '' + SELECT gid FROM grouplist WHERE username='%1$s' + ''; + description = '' + SQL query for the gidsbymem + syscall. + ''; + }; + }; + }; + }; + }; + }; + + config = mkIf cfg.enable { + system.nssModules = [ pkgs.libnss-mysql ]; + system.nssDatabases.shadow = [ "mysql" ]; + system.nssDatabases.group = [ "mysql" ]; + system.nssDatabases.passwd = [ "mysql" ]; + + environment.etc."security/pam_mysql.conf" = { + user = "root"; + group = "root"; + mode = "0600"; + # password will be added from password file in activation script + text = '' + users.host=${cfg.host} + users.db_user=${cfg.user} + users.database=${cfg.database} + users.table=${cfg.pam.table} + users.user_column=${cfg.pam.userColumn} + users.password_column=${cfg.pam.passwordColumn} + users.password_crypt=${cfg.pam.passwordCrypt} + users.disconnect_every_operation=${if cfg.pam.disconnectEveryOperation then "1" else "0"} + verbose=${if cfg.pam.verbose then "1" else "0"} + '' + optionalString (cfg.pam.cryptDefault != null) '' + users.use_${cfg.pam.cryptDefault}=1 + '' + optionalString (cfg.pam.where != null) '' + users.where_clause=${cfg.pam.where} + '' + optionalString (cfg.pam.statusColumn != null) '' + users.status_column=${cfg.pam.statusColumn} + '' + optionalString (cfg.pam.updateTable != null) '' + users.update_table=${cfg.pam.updateTable} + '' + optionalString cfg.pam.logging.enable '' + log.enabled=true + log.table=${cfg.pam.logging.table} + log.message_column=${cfg.pam.logging.msgColumn} + log.pid_column=${cfg.pam.logging.pidColumn} + log.user_column=${cfg.pam.logging.userColumn} + log.host_column=${cfg.pam.logging.hostColumn} + log.rhost_column=${cfg.pam.logging.rHostColumn} + log.time_column=${cfg.pam.logging.timeColumn} + ''; + }; + + environment.etc."libnss-mysql.cfg" = { + mode = "0600"; + user = config.services.nscd.user; + group = config.services.nscd.group; + text = optionalString (cfg.nss.getpwnam != null) '' + getpwnam ${cfg.nss.getpwnam} + '' + optionalString (cfg.nss.getpwuid != null) '' + getpwuid ${cfg.nss.getpwuid} + '' + optionalString (cfg.nss.getspnam != null) '' + getspnam ${cfg.nss.getspnam} + '' + optionalString (cfg.nss.getpwent != null) '' + getpwent ${cfg.nss.getpwent} + '' + optionalString (cfg.nss.getspent != null) '' + getspent ${cfg.nss.getspent} + '' + optionalString (cfg.nss.getgrnam != null) '' + getgrnam ${cfg.nss.getgrnam} + '' + optionalString (cfg.nss.getgrgid != null) '' + getgrgid ${cfg.nss.getgrgid} + '' + optionalString (cfg.nss.getgrent != null) '' + getgrent ${cfg.nss.getgrent} + '' + optionalString (cfg.nss.memsbygid != null) '' + memsbygid ${cfg.nss.memsbygid} + '' + optionalString (cfg.nss.gidsbymem != null) '' + gidsbymem ${cfg.nss.gidsbymem} + '' + '' + host ${cfg.host} + database ${cfg.database} + ''; + }; + + environment.etc."libnss-mysql-root.cfg" = { + mode = "0600"; + user = config.services.nscd.user; + group = config.services.nscd.group; + # password will be added from password file in activation script + text = '' + username ${cfg.user} + ''; + }; + + # Activation script to append the password from the password file + # to the configuration files. It also fixes the owner of the + # libnss-mysql-root.cfg because it is changed to root after the + # password is appended. + system.activationScripts.mysql-auth-passwords = '' + if [[ -r ${cfg.passwordFile} ]]; then + org_umask=$(umask) + umask 0077 + + conf_nss="$(mktemp)" + cp /etc/libnss-mysql-root.cfg $conf_nss + printf 'password %s\n' "$(cat ${cfg.passwordFile})" >> $conf_nss + mv -fT "$conf_nss" /etc/libnss-mysql-root.cfg + chown ${config.services.nscd.user}:${config.services.nscd.group} /etc/libnss-mysql-root.cfg + + conf_pam="$(mktemp)" + cp /etc/security/pam_mysql.conf $conf_pam + printf 'users.db_passwd=%s\n' "$(cat ${cfg.passwordFile})" >> $conf_pam + mv -fT "$conf_pam" /etc/security/pam_mysql.conf + + umask $org_umask + fi + ''; + }; +} diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index c1f435ec569c..3a849fcfec77 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -22,6 +22,7 @@ ./config/ldap.nix ./config/locale.nix ./config/malloc.nix + ./config/mysql.nix ./config/networking.nix ./config/no-x-libs.nix ./config/nsswitch.nix diff --git a/nixos/modules/security/pam.nix b/nixos/modules/security/pam.nix index 9a1acba00d0e..9f89508bd1fe 100644 --- a/nixos/modules/security/pam.nix +++ b/nixos/modules/security/pam.nix @@ -142,6 +142,16 @@ let ''; }; + mysqlAuth = mkOption { + default = config.users.mysql.enable; + defaultText = literalExpression "config.users.mysql.enable"; + type = types.bool; + description = '' + If set, the pam_mysql module will be used to + authenticate users against a MySQL/MariaDB database. + ''; + }; + fprintAuth = mkOption { default = config.services.fprintd.enable; defaultText = literalExpression "config.services.fprintd.enable"; @@ -441,11 +451,13 @@ let ( '' # Account management. - account required pam_unix.so '' + optionalString use_ldap '' account sufficient ${pam_ldap}/lib/security/pam_ldap.so '' + + optionalString cfg.mysqlAuth '' + account sufficient ${pkgs.pam_mysql}/lib/security/pam_mysql.so config_file=/etc/security/pam_mysql.conf + '' + optionalString (config.services.sssd.enable && cfg.sssdStrictAccess==false) '' account sufficient ${pkgs.sssd}/lib/security/pam_sss.so '' + @@ -459,7 +471,11 @@ let account [success=ok ignore=ignore default=die] ${pkgs.google-guest-oslogin}/lib/security/pam_oslogin_login.so account [success=ok default=ignore] ${pkgs.google-guest-oslogin}/lib/security/pam_oslogin_admin.so '' + + # The required pam_unix.so module has to come after all the sufficient modules + # because otherwise, the account lookup will fail if the user does not exist + # locally, for example with MySQL- or LDAP-auth. '' + account required pam_unix.so # Authentication management. '' + @@ -475,6 +491,9 @@ let optionalString cfg.logFailures '' auth required pam_faillock.so '' + + optionalString cfg.mysqlAuth '' + auth sufficient ${pkgs.pam_mysql}/lib/security/pam_mysql.so config_file=/etc/security/pam_mysql.conf + '' + optionalString (config.security.pam.enableSSHAgentAuth && cfg.sshAgentAuth) '' auth sufficient ${pkgs.pam_ssh_agent_auth}/libexec/pam_ssh_agent_auth.so file=${lib.concatStringsSep ":" config.services.openssh.authorizedKeysFiles} '' + @@ -572,6 +591,9 @@ let optionalString use_ldap '' password sufficient ${pam_ldap}/lib/security/pam_ldap.so '' + + optionalString cfg.mysqlAuth '' + password sufficient ${pkgs.pam_mysql}/lib/security/pam_mysql.so config_file=/etc/security/pam_mysql.conf + '' + optionalString config.services.sssd.enable '' password sufficient ${pkgs.sssd}/lib/security/pam_sss.so use_authtok '' + @@ -615,6 +637,9 @@ let optionalString use_ldap '' session optional ${pam_ldap}/lib/security/pam_ldap.so '' + + optionalString cfg.mysqlAuth '' + session optional ${pkgs.pam_mysql}/lib/security/pam_mysql.so config_file=/etc/security/pam_mysql.conf + '' + optionalString config.services.sssd.enable '' session optional ${pkgs.sssd}/lib/security/pam_sss.so '' + @@ -1236,6 +1261,9 @@ in optionalString (isEnabled (cfg: cfg.oathAuth)) '' "mr ${pkgs.oath-toolkit}/lib/security/pam_oath.so, '' + + optionalString (isEnabled (cfg: cfg.mysqlAuth)) '' + mr ${pkgs.pam_mysql}/lib/security/pam_mysql.so, + '' + optionalString (isEnabled (cfg: cfg.yubicoAuth)) '' mr ${pkgs.yubico-pam}/lib/security/pam_yubico.so, '' + diff --git a/nixos/modules/services/system/nscd.nix b/nixos/modules/services/system/nscd.nix index c3046a5b4cf4..aabcae3b6736 100644 --- a/nixos/modules/services/system/nscd.nix +++ b/nixos/modules/services/system/nscd.nix @@ -27,6 +27,22 @@ in ''; }; + user = mkOption { + type = types.str; + default = "nscd"; + description = '' + User account under which nscd runs. + ''; + }; + + group = mkOption { + type = types.str; + default = "nscd"; + description = '' + User group under which nscd runs. + ''; + }; + config = mkOption { type = types.lines; default = builtins.readFile ./nscd.conf; @@ -56,6 +72,13 @@ in config = mkIf cfg.enable { environment.etc."nscd.conf".text = cfg.config; + users.users.${cfg.user} = { + isSystemUser = true; + group = cfg.group; + }; + + users.groups.${cfg.group} = {}; + systemd.services.nscd = { description = "Name Service Cache Daemon"; @@ -69,18 +92,29 @@ in config.environment.etc.hosts.source config.environment.etc."nsswitch.conf".source config.environment.etc."nscd.conf".source + ] ++ optionals config.users.mysql.enable [ + config.environment.etc."libnss-mysql.cfg".source + config.environment.etc."libnss-mysql-root.cfg".source ]; - # We use DynamicUser because in default configurations nscd doesn't - # create any files that need to survive restarts. However, in some - # configurations, nscd needs to be started as root; it will drop - # privileges after all the NSS modules have read their configuration - # files. So prefix the ExecStart command with "!" to prevent systemd - # from dropping privileges early. See ExecStart in systemd.service(5). + # In some configurations, nscd needs to be started as root; it will + # drop privileges after all the NSS modules have read their + # configuration files. So prefix the ExecStart command with "!" to + # prevent systemd from dropping privileges early. See ExecStart in + # systemd.service(5). We use a static user, because some NSS modules + # sill want to read their configuration files after the privilege drop + # and so users can set the owner of those files to the nscd user. serviceConfig = { ExecStart = "!@${cfg.package}/bin/nscd nscd"; Type = "forking"; - DynamicUser = true; + User = cfg.user; + Group = cfg.group; + RemoveIPC = true; + PrivateTmp = true; + NoNewPrivileges = true; + RestrictSUIDSGID = true; + ProtectSystem = "strict"; + ProtectHome = "read-only"; RuntimeDirectory = "nscd"; PIDFile = "/run/nscd/nscd.pid"; Restart = "always"; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index a0af42b0056a..43fb4d2bd238 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -41,6 +41,7 @@ in { apparmor = handleTest ./apparmor.nix {}; atd = handleTest ./atd.nix {}; atop = handleTest ./atop.nix {}; + auth-mysql = handleTest ./auth-mysql.nix {}; avahi = handleTest ./avahi.nix {}; avahi-with-resolved = handleTest ./avahi.nix { networkd = true; }; babeld = handleTest ./babeld.nix {}; diff --git a/nixos/tests/auth-mysql.nix b/nixos/tests/auth-mysql.nix new file mode 100644 index 000000000000..0ed4b050a69a --- /dev/null +++ b/nixos/tests/auth-mysql.nix @@ -0,0 +1,177 @@ +import ./make-test-python.nix ({ pkgs, lib, ... }: + +let + dbUser = "nixos_auth"; + dbPassword = "topsecret123"; + dbName = "auth"; + + mysqlUsername = "mysqltest"; + mysqlPassword = "topsecretmysqluserpassword123"; + mysqlGroup = "mysqlusers"; + + localUsername = "localtest"; + localPassword = "topsecretlocaluserpassword123"; + + mysqlInit = pkgs.writeText "mysqlInit" '' + CREATE USER '${dbUser}'@'localhost' IDENTIFIED BY '${dbPassword}'; + CREATE DATABASE ${dbName}; + GRANT ALL PRIVILEGES ON ${dbName}.* TO '${dbUser}'@'localhost'; + FLUSH PRIVILEGES; + + USE ${dbName}; + CREATE TABLE `groups` ( + rowid int(11) NOT NULL auto_increment, + gid int(11) NOT NULL, + name char(255) NOT NULL, + PRIMARY KEY (rowid) + ); + + CREATE TABLE `users` ( + name varchar(255) NOT NULL, + uid int(11) NOT NULL auto_increment, + gid int(11) NOT NULL, + password varchar(255) NOT NULL, + PRIMARY KEY (uid), + UNIQUE (name) + ) AUTO_INCREMENT=5000; + + INSERT INTO `users` (name, uid, gid, password) VALUES + ('${mysqlUsername}', 5000, 5000, SHA2('${mysqlPassword}', 256)); + INSERT INTO `groups` (name, gid) VALUES ('${mysqlGroup}', 5000); + ''; +in +{ + name = "auth-mysql"; + meta.maintainers = with lib.maintainers; [ netali ]; + + nodes.machine = + { ... }: + { + services.mysql = { + enable = true; + package = pkgs.mariadb; + settings.mysqld.bind-address = "127.0.0.1"; + initialScript = mysqlInit; + }; + + users.users.${localUsername} = { + isNormalUser = true; + password = localPassword; + }; + + security.pam.services.login.makeHomeDir = true; + + users.mysql = { + enable = true; + host = "127.0.0.1"; + user = dbUser; + database = dbName; + passwordFile = "${builtins.toFile "dbPassword" dbPassword}"; + pam = { + table = "users"; + userColumn = "name"; + passwordColumn = "password"; + passwordCrypt = "sha256"; + disconnectEveryOperation = true; + }; + nss = { + getpwnam = '' + SELECT name, 'x', uid, gid, name, CONCAT('/home/', name), "/run/current-system/sw/bin/bash" \ + FROM users \ + WHERE name='%1$s' \ + LIMIT 1 + ''; + getpwuid = '' + SELECT name, 'x', uid, gid, name, CONCAT('/home/', name), "/run/current-system/sw/bin/bash" \ + FROM users \ + WHERE id=%1$u \ + LIMIT 1 + ''; + getspnam = '' + SELECT name, password, 1, 0, 99999, 7, 0, -1, 0 \ + FROM users \ + WHERE name='%1$s' \ + LIMIT 1 + ''; + getpwent = '' + SELECT name, 'x', uid, gid, name, CONCAT('/home/', name), "/run/current-system/sw/bin/bash" \ + FROM users + ''; + getspent = '' + SELECT name, password, 1, 0, 99999, 7, 0, -1, 0 \ + FROM users + ''; + getgrnam = '' + SELECT name, 'x', gid FROM groups WHERE name='%1$s' LIMIT 1 + ''; + getgrgid = '' + SELECT name, 'x', gid FROM groups WHERE gid='%1$u' LIMIT 1 + ''; + getgrent = '' + SELECT name, 'x', gid FROM groups + ''; + memsbygid = '' + SELECT name FROM users WHERE gid=%1$u + ''; + gidsbymem = '' + SELECT gid FROM users WHERE name='%1$s' + ''; + }; + }; + }; + + testScript = '' + def switch_to_tty(tty_number): + machine.fail(f"pgrep -f 'agetty.*tty{tty_number}'") + machine.send_key(f"alt-f{tty_number}") + machine.wait_until_succeeds(f"[ $(fgconsole) = {tty_number} ]") + machine.wait_for_unit(f"getty@tty{tty_number}.service") + machine.wait_until_succeeds(f"pgrep -f 'agetty.*tty{tty_number}'") + + + def try_login(tty_number, username, password): + machine.wait_until_tty_matches(tty_number, "login: ") + machine.send_chars(f"{username}\n") + machine.wait_until_tty_matches(tty_number, f"login: {username}") + machine.wait_until_succeeds("pgrep login") + machine.wait_until_tty_matches(tty_number, "Password: ") + machine.send_chars(f"{password}\n") + + + machine.wait_for_unit("multi-user.target") + machine.wait_for_unit("mysql.service") + machine.wait_until_succeeds("pgrep -f 'agetty.*tty1'") + + with subtest("Local login"): + switch_to_tty("2") + try_login("2", "${localUsername}", "${localPassword}") + + machine.wait_until_succeeds("pgrep -u ${localUsername} bash") + machine.send_chars("id > local_id.txt\n") + machine.wait_for_file("/home/${localUsername}/local_id.txt") + machine.succeed("cat /home/${localUsername}/local_id.txt | grep 'uid=1000(${localUsername}) gid=100(users) groups=100(users)'") + + with subtest("Local incorrect login"): + switch_to_tty("3") + try_login("3", "${localUsername}", "wrongpassword") + + machine.wait_until_tty_matches("3", "Login incorrect") + machine.wait_until_tty_matches("3", "login:") + + with subtest("MySQL login"): + switch_to_tty("4") + try_login("4", "${mysqlUsername}", "${mysqlPassword}") + + machine.wait_until_succeeds("pgrep -u ${mysqlUsername} bash") + machine.send_chars("id > mysql_id.txt\n") + machine.wait_for_file("/home/${mysqlUsername}/mysql_id.txt") + machine.succeed("cat /home/${mysqlUsername}/mysql_id.txt | grep 'uid=5000(${mysqlUsername}) gid=5000(${mysqlGroup}) groups=5000(${mysqlGroup})'") + + with subtest("MySQL incorrect login"): + switch_to_tty("5") + try_login("5", "${mysqlUsername}", "wrongpassword") + + machine.wait_until_tty_matches("5", "Login incorrect") + machine.wait_until_tty_matches("5", "login:") + ''; +}) diff --git a/pkgs/os-specific/linux/libnss-mysql/default.nix b/pkgs/os-specific/linux/libnss-mysql/default.nix new file mode 100644 index 000000000000..77e629b03074 --- /dev/null +++ b/pkgs/os-specific/linux/libnss-mysql/default.nix @@ -0,0 +1,30 @@ +{ lib, stdenv, fetchFromGitHub, autoreconfHook, which, libmysqlclient }: + +stdenv.mkDerivation rec { + pname = "libnss-mysql"; + version = "1.7.1"; + + src = fetchFromGitHub { + owner = "saknopper"; + repo = "libnss-mysql"; + rev = "v${version}"; + sha256 = "1fhsswa3h2nkhjkyjxxqnj07rlx6bmfvd8j521snimx2jba8h0d6"; + }; + + nativeBuildInputs = [ autoreconfHook which ]; + buildInputs = [ libmysqlclient ]; + + configureFlags = [ "--sysconfdir=/etc" ]; + installFlags = [ "sysconfdir=$(out)/etc" ]; + postInstall = '' + rm -r $out/etc + ''; + + meta = with lib; { + description = "MySQL module for the Solaris Nameservice Switch (NSS)"; + homepage = "https://github.com/saknopper/libnss-mysql"; + license = licenses.gpl2Plus; + platforms = platforms.linux; + maintainers = with maintainers; [ netali ]; + }; +} diff --git a/pkgs/os-specific/linux/pam_mysql/default.nix b/pkgs/os-specific/linux/pam_mysql/default.nix new file mode 100644 index 000000000000..807899cf2b28 --- /dev/null +++ b/pkgs/os-specific/linux/pam_mysql/default.nix @@ -0,0 +1,24 @@ +{ lib, stdenv, fetchFromGitHub, meson, ninja, pam, pkg-config, libmysqlclient, mariadb }: + +stdenv.mkDerivation rec { + pname = "pam_mysql"; + version = "1.0.0-beta2"; + + src = fetchFromGitHub { + owner = "NigelCunningham"; + repo = "pam-MySQL"; + rev = version; + sha256 = "07acf0hbhkd0kg49gnj4nb5ilnv3v4xx3dsggvzvjg8gi3cjmsap"; + }; + + nativeBuildInputs = [ meson pkg-config ninja ]; + buildInputs = [ pam libmysqlclient mariadb ]; + + meta = with lib; { + description = "PAM authentication module against a MySQL database"; + homepage = "https://github.com/NigelCunningham/pam-MySQL"; + license = licenses.gpl2Plus; + platforms = platforms.linux; + maintainers = with maintainers; [ netali ]; + }; +} diff --git a/pkgs/top-level/all-packages.nix b/pkgs/top-level/all-packages.nix index 39d7da273505..eda2b3fd9ff8 100644 --- a/pkgs/top-level/all-packages.nix +++ b/pkgs/top-level/all-packages.nix @@ -4144,6 +4144,8 @@ with pkgs; libndtypes = callPackage ../development/libraries/libndtypes { }; + libnss-mysql = callPackage ../os-specific/linux/libnss-mysql { }; + libxnd = callPackage ../development/libraries/libxnd { }; lifeograph = callPackage ../applications/editors/lifeograph { }; @@ -24378,6 +24380,8 @@ with pkgs; pam_mount = callPackage ../os-specific/linux/pam_mount { }; + pam_mysql = callPackage ../os-specific/linux/pam_mysql { }; + pam_p11 = callPackage ../os-specific/linux/pam_p11 { }; pam_pgsql = callPackage ../os-specific/linux/pam_pgsql { };