Skip to content

Commit

Permalink
Merge pull request #896 from JAORMX/tls_options
Browse files Browse the repository at this point in the history
Add support for setting tls options for mysql_user's
  • Loading branch information
EmilienM authored Oct 17, 2016
2 parents a6aff84 + e3a67ea commit 669ece6
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 13 deletions.
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ users => {
max_updates_per_hour => '0',
max_user_connections => '0',
password_hash => '*F3A2A51A9B0F2BE2468926B4132313728C250DBF',
tls_options => ['NONE'],
},
}
```
Expand Down Expand Up @@ -870,6 +871,14 @@ mysql_user{ 'myuser'@'localhost':
}
```

TLS options can be specified for a user.
```
mysql_user{ 'myuser'@'localhost':
ensure => 'present',
tls_options => ['SSL'],
}
```

##### `name`

The name of the user, as 'username@hostname' or username@hostname.
Expand All @@ -894,6 +903,10 @@ Maximum queries per hour for the user. Must be an integer value. A value of '0'

Maximum updates per hour for the user. Must be an integer value. A value of '0' specifies no (or global) limit.

##### `tls_options`

SSL-related options for a MySQL account, using one or more tls_option values. 'NONE' specifies that the account has no TLS options enforced, and the available options are 'SSL', 'X509', 'CIPHER *cipher*', 'ISSUER *issuer*', 'SUBJECT *subject*'; as stated in the MySQL documentation.


#### mysql_grant

Expand Down Expand Up @@ -1034,4 +1047,4 @@ This module is based on work by David Schmitt. The following contributors have c
* Michael Arnold
* Chris Weyl
* Daniël van Eeden
* Jan-Otto Kröpke
* Jan-Otto Kröpke
52 changes: 47 additions & 5 deletions lib/puppet/provider/mysql_user/mysql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@ def self.instances
users.collect do |name|
if mysqld_version.nil?
## Default ...
query = "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, PASSWORD /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{name}'"
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}'"
else
if (mysqld_type == "mysql" or mysqld_type == "percona") and Puppet::Util::Package.versioncmp(mysqld_version, '5.7.6') >= 0
query = "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, AUTHENTICATION_STRING, PLUGIN FROM mysql.user WHERE CONCAT(user, '@', host) = '#{name}'"
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}'"
else
query = "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, PASSWORD /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{name}'"
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
end
@max_user_connections, @max_connections_per_hour, @max_queries_per_hour,
@max_updates_per_hour, @password, @plugin = mysql([defaults_file, "-NBe", query].compact).split(/\s/)
@max_updates_per_hour, ssl_type, ssl_cipher, x509_issuer, x509_subject,
@password, @plugin = mysql([defaults_file, "-NBe", query].compact).split(/\s/)
@tls_options = parse_tls_options(ssl_type, ssl_cipher, x509_issuer, x509_subject)

new(:name => name,
:ensure => :present,
Expand All @@ -32,7 +34,8 @@ def self.instances
:max_user_connections => @max_user_connections,
:max_connections_per_hour => @max_connections_per_hour,
:max_queries_per_hour => @max_queries_per_hour,
:max_updates_per_hour => @max_updates_per_hour
:max_updates_per_hour => @max_updates_per_hour,
:tls_options => @tls_options,
)
end
end
Expand All @@ -56,6 +59,7 @@ def create
max_connections_per_hour = @resource.value(:max_connections_per_hour) || 0
max_queries_per_hour = @resource.value(:max_queries_per_hour) || 0
max_updates_per_hour = @resource.value(:max_updates_per_hour) || 0
tls_options = @resource.value(:tls_options) || ['NONE']

# Use CREATE USER to be compatible with NO_AUTO_CREATE_USER sql_mode
# This is also required if you want to specify a authentication plugin
Expand All @@ -78,6 +82,15 @@ def create
@property_hash[:max_queries_per_hour] = max_queries_per_hour
@property_hash[:max_updates_per_hour] = max_updates_per_hour

merged_tls_options = tls_options.join(' AND ')
if (((mysqld_type == "mysql" or mysqld_type == "percona") and Puppet::Util::Package.versioncmp(mysqld_version, '5.7.6') >= 0) or
(mysqld_type == 'mariadb' and Puppet::Util::Package.versioncmp(mysqld_version, '10.2.0') >= 0))
mysql([defaults_file, system_database, '-e', "ALTER USER '#{merged_name}' REQUIRE #{merged_tls_options}"].compact)
else
mysql([defaults_file, system_database, '-e', "GRANT USAGE ON *.* TO '#{merged_name}' REQUIRE #{merged_tls_options}"].compact)
end
@property_hash[:tls_options] = tls_options

exists? ? (return true) : (return false)
end

Expand Down Expand Up @@ -152,4 +165,33 @@ def max_updates_per_hour=(int)
max_updates_per_hour == int ? (return true) : (return false)
end

def tls_options=(array)
merged_name = self.class.cmd_user(@resource[:name])
merged_tls_options = array.join(' AND ')
if (((mysqld_type == "mysql" or mysqld_type == "percona") and Puppet::Util::Package.versioncmp(mysqld_version, '5.7.6') >= 0) or
(mysqld_type == 'mariadb' and Puppet::Util::Package.versioncmp(mysqld_version, '10.2.0') >= 0))
mysql([defaults_file, system_database, '-e', "ALTER USER #{merged_name} REQUIRE #{merged_tls_options}"].compact)
else
mysql([defaults_file, system_database, '-e', "GRANT USAGE ON *.* TO #{merged_name} REQUIRE #{merged_tls_options}"].compact)
end

tls_options == array ? (return true) : (return false)
end

def self.parse_tls_options(ssl_type, ssl_cipher, x509_issuer, x509_subject)
if ssl_type == 'ANY'
return ['SSL']
elsif ssl_type == 'X509'
return ['X509']
elsif ssl_type == 'SPECIFIED'
options = []
options << "CIPHER #{ssl_cipher}" if not ssl_cipher.nil? and not ssl_cipher.empty?
options << "ISSUER #{x509_issuer}" if not x509_issuer.nil? and not x509_issuer.empty?
options << "SUBJECT #{x509_subject}" if not x509_subject.nil? and not x509_subject.empty?
return options
else
return ['NONE']
end
end

end
27 changes: 27 additions & 0 deletions lib/puppet/type/mysql_user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,31 @@
newvalue(/\d+/)
end

newproperty(:tls_options, :array_matching => :all) do
desc "Options to that set the TLS-related REQUIRE attributes for the user."
validate do |value|
value = [value] if not value.is_a?(Array)
if value.include? 'NONE' or value.include? 'SSL' or value.include? 'X509'
if value.length > 1
raise(ArgumentError, "REQUIRE tls options NONE, SSL and X509 cannot be used with other options, you may only use one of them.")
end
else
value.each do |opt|
if not o = opt.match(/^(CIPHER|ISSUER|SUBJECT)/i)
raise(ArgumentError, "Invalid tls option #{o}")
end
end
end
end
def insync?(is)
# The current value may be nil and we don't
# want to call sort on it so make sure we have arrays
if is.is_a?(Array) and @should.is_a?(Array)
is.sort == @should.sort
else
is == @should
end
end
end

end
60 changes: 60 additions & 0 deletions spec/acceptance/types/mysql_user_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ class { 'mysql::server': }
expect(r.stderr).to be_empty
end
end
it 'has no SSL options' do
shell("mysql -NBe \"select SSL_TYPE from mysql.user where CONCAT(user, '@', host) = 'ashp@localhost'\"") do |r|
expect(r.stdout).to match(/^\s*$/)
expect(r.stderr).to be_empty
end
end
end
end

Expand Down Expand Up @@ -83,4 +89,58 @@ class { 'mysql::server': }
}
end
end
context 'using user-w-ssl@localhost with SSL' do
describe 'adding user' do
it 'should work without errors' do
pp = <<-EOS
mysql_user { 'user-w-ssl@localhost':
password_hash => '*F9A8E96790775D196D12F53BCC88B8048FF62ED5',
tls_options => ['SSL'],
}
EOS

apply_manifest(pp, :catch_failures => true)
end

it 'should find the user' do
shell("mysql -NBe \"select '1' from mysql.user where CONCAT(user, '@', host) = 'user-w-ssl@localhost'\"") do |r|
expect(r.stdout).to match(/^1$/)
expect(r.stderr).to be_empty
end
end
it 'should show correct ssl_type' do
shell("mysql -NBe \"select SSL_TYPE from mysql.user where CONCAT(user, '@', host) = 'user-w-ssl@localhost'\"") do |r|
expect(r.stdout).to match(/^ANY$/)
expect(r.stderr).to be_empty
end
end
end
end
context 'using user-w-x509@localhost with X509' do
describe 'adding user' do
it 'should work without errors' do
pp = <<-EOS
mysql_user { 'user-w-x509@localhost':
password_hash => '*F9A8E96790775D196D12F53BCC88B8048FF62ED5',
tls_options => ['X509'],
}
EOS

apply_manifest(pp, :catch_failures => true)
end

it 'should find the user' do
shell("mysql -NBe \"select '1' from mysql.user where CONCAT(user, '@', host) = 'user-w-x509@localhost'\"") do |r|
expect(r.stdout).to match(/^1$/)
expect(r.stderr).to be_empty
end
end
it 'should show correct ssl_type' do
shell("mysql -NBe \"select SSL_TYPE from mysql.user where CONCAT(user, '@', host) = 'user-w-x509@localhost'\"") do |r|
expect(r.stdout).to match(/^X509$/)
expect(r.stderr).to be_empty
end
end
end
end
end
53 changes: 46 additions & 7 deletions spec/unit/puppet/provider/mysql_user/mysql_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
Puppet::Util.stubs(:which).with('mysqld').returns('/usr/sbin/mysqld')
File.stubs(:file?).with('/root/.my.cnf').returns(true)
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "SELECT CONCAT(User, '@',Host) AS User FROM mysql.user"]).returns('joe@localhost')
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, PASSWORD /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = 'joe@localhost'"]).returns('10 10 10 10 *6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4')
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "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) = 'joe@localhost'"]).returns('10 10 10 10 *6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4')
end

let(:instance) { provider.class.instances.first }
Expand All @@ -98,7 +98,7 @@
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.5'][:string])
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "SELECT CONCAT(User, '@',Host) AS User FROM mysql.user"]).returns(raw_users)
parsed_users.each do |user|
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, PASSWORD /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'"]).returns('10 10 10 10 ')
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "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) = '#{user}'"]).returns('10 10 10 10 ')
end

usernames = provider.class.instances.collect {|x| x.name }
Expand All @@ -108,7 +108,7 @@
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.6'][:string])
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "SELECT CONCAT(User, '@',Host) AS User FROM mysql.user"]).returns(raw_users)
parsed_users.each do |user|
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, PASSWORD /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'"]).returns('10 10 10 10 ')
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "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) = '#{user}'"]).returns('10 10 10 10 ')
end

usernames = provider.class.instances.collect {|x| x.name }
Expand All @@ -118,7 +118,7 @@
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.1'][:string])
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "SELECT CONCAT(User, '@',Host) AS User FROM mysql.user"]).returns(raw_users)
parsed_users.each do |user|
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, PASSWORD /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'"]).returns('10 10 10 10 ')
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "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) = '#{user}'"]).returns('10 10 10 10 ')
end

usernames = provider.class.instances.collect {|x| x.name }
Expand All @@ -128,7 +128,7 @@
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.6'][:string])
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "SELECT CONCAT(User, '@',Host) AS User FROM mysql.user"]).returns(raw_users)
parsed_users.each do |user|
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, AUTHENTICATION_STRING, PLUGIN FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'"]).returns('10 10 10 10 ')
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "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) = '#{user}'"]).returns('10 10 10 10 ')
end

usernames = provider.class.instances.collect {|x| x.name }
Expand All @@ -138,7 +138,7 @@
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.0'][:string])
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "SELECT CONCAT(User, '@',Host) AS User FROM mysql.user"]).returns(raw_users)
parsed_users.each do |user|
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, PASSWORD /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'"]).returns('10 10 10 10 ')
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "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) = '#{user}'"]).returns('10 10 10 10 ')
end

usernames = provider.class.instances.collect {|x| x.name }
Expand All @@ -148,7 +148,7 @@
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['percona-5.5'][:string])
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "SELECT CONCAT(User, '@',Host) AS User FROM mysql.user"]).returns(raw_users)
parsed_users.each do |user|
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, PASSWORD /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'"]).returns('10 10 10 10 ')
provider.class.stubs(:mysql).with([defaults_file, '-NBe', "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) = '#{user}'"]).returns('10 10 10 10 ')
end

usernames = provider.class.instances.collect {|x| x.name }
Expand Down Expand Up @@ -180,6 +180,7 @@
it 'makes a user' do
provider.expects(:mysql).with([defaults_file, system_database, '-e', "CREATE USER 'joe'@'localhost' IDENTIFIED BY PASSWORD '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4'"])
provider.expects(:mysql).with([defaults_file, system_database, '-e', "GRANT USAGE ON *.* TO 'joe'@'localhost' WITH MAX_USER_CONNECTIONS 10 MAX_CONNECTIONS_PER_HOUR 10 MAX_QUERIES_PER_HOUR 10 MAX_UPDATES_PER_HOUR 10"])
provider.expects(:mysql).with([defaults_file, system_database, '-e', "GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE NONE"])
provider.expects(:exists?).returns(true)
expect(provider.create).to be_truthy
end
Expand Down Expand Up @@ -285,6 +286,44 @@
end
end

describe 'tls_options=' do
it 'adds SSL option grant in mysql 5.5' do
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.5'][:string])
provider.expects(:mysql).with([defaults_file, system_database, '-e', "GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE NONE"]).returns('0')

provider.expects(:tls_options).returns(['NONE'])
provider.tls_options=(['NONE'])
end
it 'adds SSL option grant in mysql 5.6' do
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.6'][:string])
provider.expects(:mysql).with([defaults_file, system_database, '-e', "GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE NONE"]).returns('0')

provider.expects(:tls_options).returns(['NONE'])
provider.tls_options=(['NONE'])
end
it 'adds SSL option grant in mysql < 5.7.6' do
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.1'][:string])
provider.expects(:mysql).with([defaults_file, system_database, '-e', "GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE NONE"]).returns('0')

provider.expects(:tls_options).returns(['NONE'])
provider.tls_options=(['NONE'])
end
it 'adds SSL option grant in mysql >= 5.7.6' do
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.6'][:string])
provider.expects(:mysql).with([defaults_file, system_database, '-e', "ALTER USER 'joe'@'localhost' REQUIRE NONE"]).returns('0')

provider.expects(:tls_options).returns(['NONE'])
provider.tls_options=(['NONE'])
end
it 'adds SSL option grant in mariadb-10.0' do
provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.0'][:string])
provider.expects(:mysql).with([defaults_file, system_database, '-e', "GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE NONE"]).returns('0')

provider.expects(:tls_options).returns(['NONE'])
provider.tls_options=(['NONE'])
end
end

['max_user_connections', 'max_connections_per_hour', 'max_queries_per_hour',
'max_updates_per_hour'].each do |property|

Expand Down

0 comments on commit 669ece6

Please sign in to comment.