diff --git a/lib/puppet/provider/mysql_user/mysql.rb b/lib/puppet/provider/mysql_user/mysql.rb index 7d8f43c68..1dc026d8a 100644 --- a/lib/puppet/provider/mysql_user/mysql.rb +++ b/lib/puppet/provider/mysql_user/mysql.rb @@ -14,18 +14,27 @@ def self.instances ## Default ... # rubocop:disable Metrics/LineLength query = "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, SSL_TYPE, SSL_CIPHER, X509_ISSUER, X509_SUBJECT, PASSWORD /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{name}'" - elsif newer_than('mysql' => '5.7.6', 'percona' => '5.7.6') || - # https://jira.mariadb.org/browse/MDEV-16238 https://jira.mariadb.org/browse/MDEV-16774 - (newer_than('mariadb' => '10.2.16') && older_than('mariadb' => '10.2.19')) || - (newer_than('mariadb' => '10.3.8') && older_than('mariadb' => '10.3.11')) + elsif newer_than('mysql' => '5.7.6', 'percona' => '5.7.6') query = "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, SSL_TYPE, SSL_CIPHER, X509_ISSUER, X509_SUBJECT, AUTHENTICATION_STRING, PLUGIN FROM mysql.user WHERE CONCAT(user, '@', host) = '#{name}'" + elsif newer_than('mariadb' => '10.1.21') + query = "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, SSL_TYPE, SSL_CIPHER, X509_ISSUER, X509_SUBJECT, PASSWORD, PLUGIN, AUTHENTICATION_STRING FROM mysql.user WHERE CONCAT(user, '@', host) = '#{name}'" else query = "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, SSL_TYPE, SSL_CIPHER, X509_ISSUER, X509_SUBJECT, PASSWORD /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{name}'" end @max_user_connections, @max_connections_per_hour, @max_queries_per_hour, @max_updates_per_hour, ssl_type, ssl_cipher, x509_issuer, x509_subject, - @password, @plugin = mysql_caller(query, 'regular').split(%r{\s}) + @password, @plugin, @authentication_string = mysql_caller(query, 'regular').split(%r{\s}) @tls_options = parse_tls_options(ssl_type, ssl_cipher, x509_issuer, x509_subject) + if newer_than('mariadb' => '10.1.21') && @plugin == 'ed25519' + # Some auth plugins (e.g. ed25519) use authentication_string + # to store password hash or auth information + @password = @authentication_string + elsif (newer_than('mariadb' => '10.2.16') && older_than('mariadb' => '10.2.19')) || + (newer_than('mariadb' => '10.3.8') && older_than('mariadb' => '10.3.11')) + # Old mariadb 10.2 or 10.3 store password hash in authentication_string + # https://jira.mariadb.org/browse/MDEV-16238 https://jira.mariadb.org/browse/MDEV-16774 + @password = @authentication_string + end # rubocop:enable Metrics/LineLength new(name: name, ensure: :present, @@ -133,11 +142,25 @@ def exists? def password_hash=(string) merged_name = self.class.cmd_user(@resource[:name]) + plugin = @resource.value(:plugin) # We have a fact for the mysql version ... if mysqld_version.nil? # default ... if mysqld_version does not work self.class.mysql_caller("SET PASSWORD FOR #{merged_name} = '#{string}'", 'system') + elsif newer_than('mariadb' => '10.1.21') && plugin == 'ed25519' + raise ArgumentError, _('ed25519 hash should be 43 bytes long.') unless string.length == 43 + # ALTER USER statement is only available upstream starting 10.2 + # https://mariadb.com/kb/en/mariadb-1020-release-notes/ + if newer_than('mariadb' => '10.2.0') + sql = "ALTER USER #{merged_name} IDENTIFIED WITH ed25519 AS '#{string}'" + else + concat_name = @resource[:name] + sql = "UPDATE mysql.user SET password = '', plugin = 'ed25519'" + sql << ", authentication_string = '#{string}'" + sql << " where CONCAT(user, '@', host) = '#{concat_name}'; FLUSH PRIVILEGES" + end + self.class.mysql_caller(sql, 'system') elsif newer_than('mysql' => '5.7.6', 'percona' => '5.7.6', 'mariadb' => '10.2.0') raise ArgumentError, _('Only mysql_native_password (*ABCD...XXX) hashes are supported.') unless string =~ %r{^\*|^$} self.class.mysql_caller("ALTER USER #{merged_name} IDENTIFIED WITH mysql_native_password AS '#{string}'", 'system') @@ -179,7 +202,16 @@ def max_updates_per_hour=(int) def plugin=(string) merged_name = self.class.cmd_user(@resource[:name]) - if newer_than('mysql' => '5.7.6', 'percona' => '5.7.6') + if newer_than('mariadb' => '10.1.21') && string == 'ed25519' + if newer_than('mariadb' => '10.2.0') + sql = "ALTER USER #{merged_name} IDENTIFIED WITH '#{string}' AS '#{@resource[:password_hash]}'" + else + concat_name = @resource[:name] + sql = "UPDATE mysql.user SET password = '', plugin = '#{string}'" + sql << ", authentication_string = '#{@resource[:password_hash]}'" + sql << " where CONCAT(user, '@', host) = '#{concat_name}'; FLUSH PRIVILEGES" + end + elsif newer_than('mysql' => '5.7.6', 'percona' => '5.7.6') sql = "ALTER USER #{merged_name} IDENTIFIED WITH '#{string}'" sql << " AS '#{@resource[:password_hash]}'" if string == 'mysql_native_password' else diff --git a/spec/acceptance/types/mysql_user_spec.rb b/spec/acceptance/types/mysql_user_spec.rb index cd87332d7..88eb79833 100644 --- a/spec/acceptance/types/mysql_user_spec.rb +++ b/spec/acceptance/types/mysql_user_spec.rb @@ -3,7 +3,14 @@ describe 'mysql_user' do describe 'setup' do pp_one = <<-MANIFEST - class { 'mysql::server': } + $ed25519_opts = versioncmp($facts['mysql_version'], '10.1.21') >= 0 ? { + true => { + restart => true, + override_options => { 'mysqld' => { 'plugin_load_add' => 'auth_ed25519' } }, + }, + false => {} + } + class { 'mysql::server': * => $ed25519_opts } MANIFEST it 'works with no errors' do apply_manifest(pp_one, catch_failures: true) @@ -67,6 +74,26 @@ class { 'mysql::server': } end end end + + describe 'using ed25519 authentication plugin', if: Gem::Version.new(mysql_version) > Gem::Version.new('10.1.21') do + it 'works without errors' do + pp = <<-EOS + mysql_user { 'ashp@localhost': + plugin => 'ed25519', + password_hash => 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU', + } + EOS + + idempotent_apply(pp) + end + + it 'has the correct plugin' do + run_shell("mysql -NBe \"select plugin from mysql.user where CONCAT(user, '@', host) = 'ashp@localhost'\"") do |r| + expect(r.stdout.rstrip).to eq('ed25519') + expect(r.stderr).to be_empty + end + end + end # rubocop:enable RSpec/ExampleLength, RSpec/MultipleExpectations end diff --git a/spec/unit/puppet/provider/mysql_user/mysql_spec.rb b/spec/unit/puppet/provider/mysql_user/mysql_spec.rb index 61af6f8c3..648485ebf 100644 --- a/spec/unit/puppet/provider/mysql_user/mysql_spec.rb +++ b/spec/unit/puppet/provider/mysql_user/mysql_spec.rb @@ -39,6 +39,18 @@ string: '/usr/sbin/mysqld (mysqld 10.0.23-MariaDB-0+deb8u1)', mysql_type: 'mariadb', }, + 'mariadb-10.1.44' => + { + version: '10.1.44', + string: '/usr/sbin/mysqld (mysqld 10.1.44-MariaDB-1~bionic)', + mysql_type: 'mariadb', + }, + 'mariadb-10.3.22' => + { + version: '10.3.22', + string: '/usr/sbin/mysqld (mysqld 10.3.22-MariaDB-0+deb10u1)', + mysql_type: 'mariadb', + }, 'percona-5.5' => { version: '5.5.39', @@ -133,6 +145,14 @@ usernames = provider.class.instances.map { |x| x.name } expect(parsed_users).to match_array(usernames) end + it 'returns an array of users mariadb >= 10.1.21' do + provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.1.44'][:string]) + provider.class.stubs(:mysql_caller).with("SELECT CONCAT(User, '@',Host) AS User FROM mysql.user", 'regular').returns(raw_users) + parsed_users.each { |user| provider.class.stubs(:mysql_caller).with("SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, SSL_TYPE, SSL_CIPHER, X509_ISSUER, X509_SUBJECT, PASSWORD, PLUGIN, AUTHENTICATION_STRING FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'", 'regular').returns('10 10 10 10 ') } # rubocop:disable Metrics/LineLength + + usernames = provider.class.instances.map { |x| x.name } + expect(parsed_users).to match_array(usernames) + end it 'returns an array of users percona 5.5' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['percona-5.5'][:string]) provider.class.stubs(:mysql_caller).with("SELECT CONCAT(User, '@',Host) AS User FROM mysql.user", 'regular').returns(raw_users) @@ -282,6 +302,25 @@ provider.expects(:password_hash).returns('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5') provider.password_hash = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5' end + it 'changes the hash to an ed25519 hash mariadb >= 10.1.21 and < 10.2.0' do + provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.1.44'][:string]) + resource.stubs(:value).with(:plugin).returns('ed25519') + provider.class.expects(:mysql_caller).with("UPDATE mysql.user SET password = '', plugin = 'ed25519', authentication_string = 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU' where CONCAT(user, '@', host) = 'joe@localhost'; FLUSH PRIVILEGES", 'system').returns('0') # rubocop:disable Metrics/LineLength + provider.expects(:password_hash).returns('z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU') + provider.password_hash = 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU' + end + it 'changes the hash to an ed25519 hash mariadb >= 10.2.0' do + provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.3.22'][:string]) + resource.stubs(:value).with(:plugin).returns('ed25519') + provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' IDENTIFIED WITH ed25519 AS 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU'", 'system').returns('0') # rubocop:disable Metrics/LineLength + provider.expects(:password_hash).returns('z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU') + provider.password_hash = 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU' + end + it 'changes the hash to an invalid ed25519 hash mariadb >= 10.1.21' do + provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.1.44'][:string]) + resource.stubs(:value).with(:plugin).returns('ed25519') + expect { provider.password_hash = 'invalid' }.to raise_error(ArgumentError, 'ed25519 hash should be 43 bytes long.') + end it 'changes the hash percona-5.5' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['percona-5.5'][:string]) provider.class.expects(:mysql_caller).with("SET PASSWORD FOR 'joe'@'localhost' = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5'", 'system').returns('0') @@ -335,6 +374,30 @@ end end end + + context 'ed25519' do + context 'mariadb >= 10.1.21 and < 10.2.0' do + it 'changes the authentication plugin' do + provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.1.44'][:string]) + resource.stubs('[]').with(:name).returns('joe@localhost') + resource.stubs('[]').with(:password_hash).returns('z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU') + provider.class.expects(:mysql_caller).with("UPDATE mysql.user SET password = '', plugin = 'ed25519', authentication_string = 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU' where CONCAT(user, '@', host) = 'joe@localhost'; FLUSH PRIVILEGES", 'system').returns('0') # rubocop:disable Metrics/LineLength + provider.expects(:plugin).returns('ed25519') + provider.plugin = 'ed25519' + end + end + + context 'mariadb >= 10.2.0' do + it 'changes the authentication plugin' do + provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.3.22'][:string]) + resource.stubs('[]').with(:name).returns('joe@localhost') + resource.stubs('[]').with(:password_hash).returns('z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU') + provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' IDENTIFIED WITH 'ed25519' AS 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU'", 'system').returns('0') # rubocop:disable Metrics/LineLength + provider.expects(:plugin).returns('ed25519') + provider.plugin = 'ed25519' + end + end + end # rubocop:enable RSpec/NestedGroups end