Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support mariadb's ed25519-based authentication #1292

Merged
merged 1 commit into from
Mar 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 38 additions & 6 deletions lib/puppet/provider/mysql_user/mysql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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
Expand Down
29 changes: 28 additions & 1 deletion spec/acceptance/types/mysql_user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
63 changes: 63 additions & 0 deletions spec/unit/puppet/provider/mysql_user/mysql_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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

Expand Down