Skip to content

Commit b53e398

Browse files
committed
Support mariadb's ed25519 authentication plugin
Support the creation and updating of mysql users when the authentication plugin is set to ed25519.
1 parent 312aca8 commit b53e398

File tree

3 files changed

+88
-7
lines changed

3 files changed

+88
-7
lines changed

Diff for: lib/puppet/provider/mysql_user/mysql.rb

+21-6
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,27 @@ def self.instances
1414
## Default ...
1515
# rubocop:disable Metrics/LineLength
1616
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}'"
17-
elsif newer_than('mysql' => '5.7.6', 'percona' => '5.7.6') ||
18-
# https://jira.mariadb.org/browse/MDEV-16238 https://jira.mariadb.org/browse/MDEV-16774
19-
(newer_than('mariadb' => '10.2.16') && older_than('mariadb' => '10.2.19')) ||
20-
(newer_than('mariadb' => '10.3.8') && older_than('mariadb' => '10.3.11'))
17+
elsif newer_than('mysql' => '5.7.6', 'percona' => '5.7.6')
2118
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}'"
19+
elsif newer_than('mariadb' => '10.1.21')
20+
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}'"
2221
else
2322
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}'"
2423
end
2524
@max_user_connections, @max_connections_per_hour, @max_queries_per_hour,
2625
@max_updates_per_hour, ssl_type, ssl_cipher, x509_issuer, x509_subject,
27-
@password, @plugin = mysql_caller(query, 'regular').split(%r{\s})
26+
@password, @plugin, @authentication_string = mysql_caller(query, 'regular').split(%r{\s})
2827
@tls_options = parse_tls_options(ssl_type, ssl_cipher, x509_issuer, x509_subject)
28+
if newer_than('mariadb' => '10.1.21') && ![nil, '', 'mysql_native_password'].include?(@plugin)
29+
# if an specific auth plugin is used (e.g. ed25519),
30+
# use authentication_string as the hash
31+
@password = @authentication_string
32+
elsif (newer_than('mariadb' => '10.2.16') && older_than('mariadb' => '10.2.19')) ||
33+
(newer_than('mariadb' => '10.3.8') && older_than('mariadb' => '10.3.11'))
34+
# old mariadb 10.2 or 10.3 store password in authentication_string
35+
# https://jira.mariadb.org/browse/MDEV-16238 https://jira.mariadb.org/browse/MDEV-16774
36+
@password = @authentication_string
37+
end
2938
# rubocop:enable Metrics/LineLength
3039
new(name: name,
3140
ensure: :present,
@@ -133,11 +142,15 @@ def exists?
133142

134143
def password_hash=(string)
135144
merged_name = self.class.cmd_user(@resource[:name])
145+
plugin = @resource.value(:plugin)
136146

137147
# We have a fact for the mysql version ...
138148
if mysqld_version.nil?
139149
# default ... if mysqld_version does not work
140150
self.class.mysql_caller("SET PASSWORD FOR #{merged_name} = '#{string}'", 'system')
151+
elsif newer_than('mariadb' => '10.1.21') && plugin == 'ed25519'
152+
raise ArgumentError, _('ed25519 hash should be 43 bytes long.') unless string.length == 43
153+
self.class.mysql_caller("ALTER USER #{merged_name} IDENTIFIED WITH ed25519 AS '#{string}'", 'system')
141154
elsif newer_than('mysql' => '5.7.6', 'percona' => '5.7.6', 'mariadb' => '10.2.0')
142155
raise ArgumentError, _('Only mysql_native_password (*ABCD...XXX) hashes are supported.') unless string =~ %r{^\*|^$}
143156
self.class.mysql_caller("ALTER USER #{merged_name} IDENTIFIED WITH mysql_native_password AS '#{string}'", 'system')
@@ -179,7 +192,9 @@ def max_updates_per_hour=(int)
179192
def plugin=(string)
180193
merged_name = self.class.cmd_user(@resource[:name])
181194

182-
if newer_than('mysql' => '5.7.6', 'percona' => '5.7.6')
195+
if newer_than('mariadb' => '10.1.21') && string == 'ed25519'
196+
sql = "ALTER USER #{merged_name} IDENTIFIED WITH '#{string}' AS '#{@resource[:password_hash]}'"
197+
elsif newer_than('mysql' => '5.7.6', 'percona' => '5.7.6')
183198
sql = "ALTER USER #{merged_name} IDENTIFIED WITH '#{string}'"
184199
sql << " AS '#{@resource[:password_hash]}'" if string == 'mysql_native_password'
185200
else

Diff for: spec/acceptance/types/mysql_user_spec.rb

+28-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33
describe 'mysql_user' do
44
describe 'setup' do
55
pp_one = <<-MANIFEST
6-
class { 'mysql::server': }
6+
$ed25519_opts = versioncmp($facts['mysql_version'], '10.1.21') >= 0 ? {
7+
true => {
8+
restart => true,
9+
override_options => { 'mysqld' => { 'plugin_load_add' => 'auth_ed25519' } },
10+
},
11+
false => {}
12+
}
13+
class { 'mysql::server': * => $ed25519_opts }
714
MANIFEST
815
it 'works with no errors' do
916
apply_manifest(pp_one, catch_failures: true)
@@ -67,6 +74,26 @@ class { 'mysql::server': }
6774
end
6875
end
6976
end
77+
78+
describe 'using ed25519 authentication plugin', if: Gem::Version.new(mysql_version) > Gem::Version.new('10.1.21') do
79+
it 'works without errors' do
80+
pp = <<-EOS
81+
mysql_user { 'ashp@localhost':
82+
plugin => 'ed25519',
83+
password_hash => 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU',
84+
}
85+
EOS
86+
87+
idempotent_apply(pp)
88+
end
89+
90+
it 'has the correct plugin' do
91+
run_shell("mysql -NBe \"select plugin from mysql.user where CONCAT(user, '@', host) = 'ashp@localhost'\"") do |r|
92+
expect(r.stdout.rstrip).to eq('ed25519')
93+
expect(r.stderr).to be_empty
94+
end
95+
end
96+
end
7097
# rubocop:enable RSpec/ExampleLength, RSpec/MultipleExpectations
7198
end
7299

Diff for: spec/unit/puppet/provider/mysql_user/mysql_spec.rb

+39
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@
3939
string: '/usr/sbin/mysqld (mysqld 10.0.23-MariaDB-0+deb8u1)',
4040
mysql_type: 'mariadb',
4141
},
42+
'mariadb-10.1.44' =>
43+
{
44+
version: '10.1.44',
45+
string: '/usr/sbin/mysqld (mysqld 10.1.44-MariaDB-1~bionic)',
46+
mysql_type: 'mariadb',
47+
},
4248
'percona-5.5' =>
4349
{
4450
version: '5.5.39',
@@ -133,6 +139,14 @@
133139
usernames = provider.class.instances.map { |x| x.name }
134140
expect(parsed_users).to match_array(usernames)
135141
end
142+
it 'returns an array of users mariadb >= 10.1.21' do
143+
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.1.44'][:string])
144+
provider.class.stubs(:mysql_caller).with("SELECT CONCAT(User, '@',Host) AS User FROM mysql.user", 'regular').returns(raw_users)
145+
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
146+
147+
usernames = provider.class.instances.map { |x| x.name }
148+
expect(parsed_users).to match_array(usernames)
149+
end
136150
it 'returns an array of users percona 5.5' do
137151
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['percona-5.5'][:string])
138152
provider.class.stubs(:mysql_caller).with("SELECT CONCAT(User, '@',Host) AS User FROM mysql.user", 'regular').returns(raw_users)
@@ -282,6 +296,18 @@
282296
provider.expects(:password_hash).returns('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5')
283297
provider.password_hash = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5'
284298
end
299+
it 'changes the hash to an ed25519 hash mariadb >= 10.1.21' do
300+
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.1.44'][:string])
301+
resource.stubs(:value).with(:plugin).returns('ed25519')
302+
provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' IDENTIFIED WITH ed25519 AS 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU'", 'system').returns('0') # rubocop:disable Metrics/LineLength
303+
provider.expects(:password_hash).returns('z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU')
304+
provider.password_hash = 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU'
305+
end
306+
it 'changes the hash to an invalid ed25519 hash mariadb >= 10.1.21' do
307+
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.1.44'][:string])
308+
resource.stubs(:value).with(:plugin).returns('ed25519')
309+
expect { provider.password_hash = 'invalid' }.to raise_error(ArgumentError, 'ed25519 hash should be 43 bytes long.')
310+
end
285311
it 'changes the hash percona-5.5' do
286312
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['percona-5.5'][:string])
287313
provider.class.expects(:mysql_caller).with("SET PASSWORD FOR 'joe'@'localhost' = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5'", 'system').returns('0')
@@ -335,6 +361,19 @@
335361
end
336362
end
337363
end
364+
365+
context 'ed25519' do
366+
context 'mariadb >= 10.1.21' do
367+
it 'changes the authentication plugin' do
368+
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.1.44'][:string])
369+
resource.stubs('[]').with(:name).returns("'joe'@'localhost'")
370+
resource.stubs('[]').with(:password_hash).returns('z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU')
371+
provider.class.expects(:mysql_caller).with("ALTER USER ''joe''@''localhost'' IDENTIFIED WITH 'ed25519' AS 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU'", 'system').returns('0') # rubocop:disable Metrics/LineLength
372+
provider.expects(:plugin).returns('ed25519')
373+
provider.plugin = 'ed25519'
374+
end
375+
end
376+
end
338377
# rubocop:enable RSpec/NestedGroups
339378
end
340379

0 commit comments

Comments
 (0)