Skip to content

Commit 95f9b98

Browse files
Merge pull request #1292 from dciabrin/ed25519
Support mariadb's ed25519-based authentication
2 parents 84069b0 + 6b6fe8b commit 95f9b98

File tree

3 files changed

+129
-7
lines changed

3 files changed

+129
-7
lines changed

lib/puppet/provider/mysql_user/mysql.rb

+38-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') && @plugin == 'ed25519'
29+
# Some auth plugins (e.g. ed25519) use authentication_string
30+
# to store password hash or auth information
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 hash 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,25 @@ 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+
# ALTER USER statement is only available upstream starting 10.2
154+
# https://mariadb.com/kb/en/mariadb-1020-release-notes/
155+
if newer_than('mariadb' => '10.2.0')
156+
sql = "ALTER USER #{merged_name} IDENTIFIED WITH ed25519 AS '#{string}'"
157+
else
158+
concat_name = @resource[:name]
159+
sql = "UPDATE mysql.user SET password = '', plugin = 'ed25519'"
160+
sql << ", authentication_string = '#{string}'"
161+
sql << " where CONCAT(user, '@', host) = '#{concat_name}'; FLUSH PRIVILEGES"
162+
end
163+
self.class.mysql_caller(sql, 'system')
141164
elsif newer_than('mysql' => '5.7.6', 'percona' => '5.7.6', 'mariadb' => '10.2.0')
142165
raise ArgumentError, _('Only mysql_native_password (*ABCD...XXX) hashes are supported.') unless string =~ %r{^\*|^$}
143166
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)
179202
def plugin=(string)
180203
merged_name = self.class.cmd_user(@resource[:name])
181204

182-
if newer_than('mysql' => '5.7.6', 'percona' => '5.7.6')
205+
if newer_than('mariadb' => '10.1.21') && string == 'ed25519'
206+
if newer_than('mariadb' => '10.2.0')
207+
sql = "ALTER USER #{merged_name} IDENTIFIED WITH '#{string}' AS '#{@resource[:password_hash]}'"
208+
else
209+
concat_name = @resource[:name]
210+
sql = "UPDATE mysql.user SET password = '', plugin = '#{string}'"
211+
sql << ", authentication_string = '#{@resource[:password_hash]}'"
212+
sql << " where CONCAT(user, '@', host) = '#{concat_name}'; FLUSH PRIVILEGES"
213+
end
214+
elsif newer_than('mysql' => '5.7.6', 'percona' => '5.7.6')
183215
sql = "ALTER USER #{merged_name} IDENTIFIED WITH '#{string}'"
184216
sql << " AS '#{@resource[:password_hash]}'" if string == 'mysql_native_password'
185217
else

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

spec/unit/puppet/provider/mysql_user/mysql_spec.rb

+63
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,18 @@
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+
},
48+
'mariadb-10.3.22' =>
49+
{
50+
version: '10.3.22',
51+
string: '/usr/sbin/mysqld (mysqld 10.3.22-MariaDB-0+deb10u1)',
52+
mysql_type: 'mariadb',
53+
},
4254
'percona-5.5' =>
4355
{
4456
version: '5.5.39',
@@ -133,6 +145,14 @@
133145
usernames = provider.class.instances.map { |x| x.name }
134146
expect(parsed_users).to match_array(usernames)
135147
end
148+
it 'returns an array of users mariadb >= 10.1.21' do
149+
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.1.44'][:string])
150+
provider.class.stubs(:mysql_caller).with("SELECT CONCAT(User, '@',Host) AS User FROM mysql.user", 'regular').returns(raw_users)
151+
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
152+
153+
usernames = provider.class.instances.map { |x| x.name }
154+
expect(parsed_users).to match_array(usernames)
155+
end
136156
it 'returns an array of users percona 5.5' do
137157
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['percona-5.5'][:string])
138158
provider.class.stubs(:mysql_caller).with("SELECT CONCAT(User, '@',Host) AS User FROM mysql.user", 'regular').returns(raw_users)
@@ -282,6 +302,25 @@
282302
provider.expects(:password_hash).returns('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5')
283303
provider.password_hash = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5'
284304
end
305+
it 'changes the hash to an ed25519 hash mariadb >= 10.1.21 and < 10.2.0' do
306+
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.1.44'][:string])
307+
resource.stubs(:value).with(:plugin).returns('ed25519')
308+
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
309+
provider.expects(:password_hash).returns('z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU')
310+
provider.password_hash = 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU'
311+
end
312+
it 'changes the hash to an ed25519 hash mariadb >= 10.2.0' do
313+
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.3.22'][:string])
314+
resource.stubs(:value).with(:plugin).returns('ed25519')
315+
provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' IDENTIFIED WITH ed25519 AS 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU'", 'system').returns('0') # rubocop:disable Metrics/LineLength
316+
provider.expects(:password_hash).returns('z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU')
317+
provider.password_hash = 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU'
318+
end
319+
it 'changes the hash to an invalid ed25519 hash mariadb >= 10.1.21' do
320+
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.1.44'][:string])
321+
resource.stubs(:value).with(:plugin).returns('ed25519')
322+
expect { provider.password_hash = 'invalid' }.to raise_error(ArgumentError, 'ed25519 hash should be 43 bytes long.')
323+
end
285324
it 'changes the hash percona-5.5' do
286325
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['percona-5.5'][:string])
287326
provider.class.expects(:mysql_caller).with("SET PASSWORD FOR 'joe'@'localhost' = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5'", 'system').returns('0')
@@ -335,6 +374,30 @@
335374
end
336375
end
337376
end
377+
378+
context 'ed25519' do
379+
context 'mariadb >= 10.1.21 and < 10.2.0' do
380+
it 'changes the authentication plugin' do
381+
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.1.44'][:string])
382+
resource.stubs('[]').with(:name).returns('joe@localhost')
383+
resource.stubs('[]').with(:password_hash).returns('z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU')
384+
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
385+
provider.expects(:plugin).returns('ed25519')
386+
provider.plugin = 'ed25519'
387+
end
388+
end
389+
390+
context 'mariadb >= 10.2.0' do
391+
it 'changes the authentication plugin' do
392+
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.3.22'][:string])
393+
resource.stubs('[]').with(:name).returns('joe@localhost')
394+
resource.stubs('[]').with(:password_hash).returns('z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU')
395+
provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' IDENTIFIED WITH 'ed25519' AS 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU'", 'system').returns('0') # rubocop:disable Metrics/LineLength
396+
provider.expects(:plugin).returns('ed25519')
397+
provider.plugin = 'ed25519'
398+
end
399+
end
400+
end
338401
# rubocop:enable RSpec/NestedGroups
339402
end
340403

0 commit comments

Comments
 (0)