diff --git a/lib/puppet/functions/mysql/normalise_and_deepmerge.rb b/lib/puppet/functions/mysql/normalise_and_deepmerge.rb index 5f8d437..c2f5880 100644 --- a/lib/puppet/functions/mysql/normalise_and_deepmerge.rb +++ b/lib/puppet/functions/mysql/normalise_and_deepmerge.rb @@ -1,69 +1,69 @@ # frozen_string_literal: true # @summary Recursively merges two or more hashes together, normalises keys with differing use of dashesh and underscores, # then returns the resulting hash. # # @example # $hash1 = {'one' => 1, 'two' => 2, 'three' => { 'four' => 4 } } # $hash2 = {'two' => 'dos', 'three' => { 'five' => 5 } } # $merged_hash = mysql::normalise_and_deepmerge($hash1, $hash2) # # The resulting hash is equivalent to: # # $merged_hash = { 'one' => 1, 'two' => 'dos', 'three' => { 'four' => 4, 'five' => 5 } } # # - When there is a duplicate key that is a hash, they are recursively merged. # - When there is a duplicate key that is not a hash, the key in the rightmost hash will "win." # - When there are conficting uses of dashes and underscores in two keys (which mysql would otherwise equate), the rightmost style will win. # Puppet::Functions.create_function(:'mysql::normalise_and_deepmerge') do def normalise_and_deepmerge(*args) if args.length < 2 raise Puppet::ParseError, _('mysql::normalise_and_deepmerge(): wrong number of arguments (%{args_length}; must be at least 2)') % { args_length: args.length } end result = {} args.each do |arg| next if arg.is_a?(String) && arg.empty? # empty string is synonym for puppet's undef # If the argument was not a hash, skip it. unless arg.is_a?(Hash) raise Puppet::ParseError, _('mysql::normalise_and_deepmerge: unexpected argument type %{arg_class}, only expects hash arguments.') % { args_class: args.class } end # We need to make a copy of the hash since it is frozen by puppet current = deep_copy(arg) # Now we have to traverse our hash assigning our non-hash values # to the matching keys in our result while following our hash values # and repeating the process. overlay(result, current) end result end def normalized?(hash, key) return true if hash.key?(key) - return false unless key =~ %r{-|_} + return false unless %r{-|_}.match?(key) other_key = key.include?('-') ? key.tr('-', '_') : key.tr('_', '-') return false unless hash.key?(other_key) hash[key] = hash.delete(other_key) true end def overlay(hash1, hash2) hash2.each do |key, value| if normalized?(hash1, key) && value.is_a?(Hash) && hash1[key].is_a?(Hash) overlay(hash1[key], value) else hash1[key] = value end end end def deep_copy(inputhash) return inputhash unless inputhash.is_a? Hash hash = {} inputhash.each do |k, v| hash.store(k, deep_copy(v)) end hash end end diff --git a/lib/puppet/functions/mysql/password.rb b/lib/puppet/functions/mysql/password.rb index e0d3756..8cb9e55 100644 --- a/lib/puppet/functions/mysql/password.rb +++ b/lib/puppet/functions/mysql/password.rb @@ -1,24 +1,24 @@ # frozen_string_literal: true require 'digest/sha1' # @summary # Hash a string as mysql's "PASSWORD()" function would do it # Puppet::Functions.create_function(:'mysql::password') do # @param password # Plain text password. # # @return hash # The mysql password hash from the clear text password. # dispatch :password do required_param 'String', :password return_type 'String' end def password(password) return '' if password.empty? - return password if password =~ %r{\*[A-F0-9]{40}$} + return password if %r{\*[A-F0-9]{40}$}.match?(password) '*' + Digest::SHA1.hexdigest(Digest::SHA1.digest(password)).upcase end end diff --git a/lib/puppet/provider/mysql.rb b/lib/puppet/provider/mysql.rb index f99b8f5..0a6d581 100644 --- a/lib/puppet/provider/mysql.rb +++ b/lib/puppet/provider/mysql.rb @@ -1,177 +1,177 @@ # frozen_string_literal: true # Puppet provider for mysql class Puppet::Provider::Mysql < Puppet::Provider # Without initvars commands won't work. initvars # Make sure we find mysql commands on CentOS and FreeBSD ENV['PATH'] = ENV['PATH'] + ':/usr/libexec:/usr/local/libexec:/usr/local/bin' ENV['LD_LIBRARY_PATH'] = [ ENV['LD_LIBRARY_PATH'], '/usr/lib', '/usr/lib64', '/opt/rh/rh-mysql56/root/usr/lib', '/opt/rh/rh-mysql56/root/usr/lib64', '/opt/rh/rh-mysql57/root/usr/lib', '/opt/rh/rh-mysql57/root/usr/lib64', '/opt/rh/rh-mysql80/root/usr/lib', '/opt/rh/rh-mysql80/root/usr/lib64', '/opt/rh/rh-mariadb100/root/usr/lib', '/opt/rh/rh-mariadb100/root/usr/lib64', '/opt/rh/rh-mariadb101/root/usr/lib', '/opt/rh/rh-mariadb101/root/usr/lib64', '/opt/rh/rh-mariadb102/root/usr/lib', '/opt/rh/rh-mariadb102/root/usr/lib64', '/opt/rh/rh-mariadb103/root/usr/lib', '/opt/rh/rh-mariadb103/root/usr/lib64', '/opt/rh/mysql55/root/usr/lib', '/opt/rh/mysql55/root/usr/lib64', '/opt/rh/mariadb55/root/usr/lib', '/opt/rh/mariadb55/root/usr/lib64', '/usr/mysql/5.5/lib', '/usr/mysql/5.5/lib64', '/usr/mysql/5.6/lib', '/usr/mysql/5.6/lib64', '/usr/mysql/5.7/lib', '/usr/mysql/5.7/lib64', ].join(':') # rubocop:disable Style/HashSyntax commands :mysql_raw => 'mysql' commands :mysqld => 'mysqld' commands :mysqladmin => 'mysqladmin' # rubocop:enable Style/HashSyntax # Optional defaults file def self.defaults_file "--defaults-extra-file=#{Facter.value(:root_home)}/.my.cnf" if File.file?("#{Facter.value(:root_home)}/.my.cnf") end def self.mysqld_type # find the mysql "dialect" like mariadb / mysql etc. mysqld_version_string.scan(%r{mariadb}i) { return 'mariadb' } mysqld_version_string.scan(%r{\s\(percona}i) { return 'percona' } 'mysql' end def mysqld_type self.class.mysqld_type end def self.mysqld_version_string # As the possibility of the mysqld being remote we need to allow the version string to be overridden, # this can be done by facter.value as seen below. In the case that it has not been set and the facter # value is nil we use the mysql -v command to ensure we report the correct version of mysql for later use cases. @mysqld_version_string ||= Facter.value(:mysqld_version) || mysqld('-V') end def mysqld_version_string self.class.mysqld_version_string end def self.mysqld_version - # note: be prepared for '5.7.6-rc-log' etc results + # NOTE: be prepared for '5.7.6-rc-log' etc results # versioncmp detects 5.7.6-log to be newer then 5.7.6 # this is why we need the trimming. mysqld_version_string.scan(%r{\d+\.\d+\.\d+}).first unless mysqld_version_string.nil? end def mysqld_version self.class.mysqld_version end def self.newer_than(forks_versions) forks_versions.keys.include?(mysqld_type) && Puppet::Util::Package.versioncmp(mysqld_version, forks_versions[mysqld_type]) >= 0 end def newer_than(forks_versions) self.class.newer_than(forks_versions) end def self.older_than(forks_versions) forks_versions.keys.include?(mysqld_type) && Puppet::Util::Package.versioncmp(mysqld_version, forks_versions[mysqld_type]) < 0 end def older_than(forks_versions) self.class.older_than(forks_versions) end def defaults_file self.class.defaults_file end def self.mysql_caller(text_of_sql, type) if type.eql? 'system' if File.file?("#{Facter.value(:root_home)}/.mylogin.cnf") ENV['MYSQL_TEST_LOGIN_FILE'] = "#{Facter.value(:root_home)}/.mylogin.cnf" mysql_raw([system_database, '-e', text_of_sql].flatten.compact).scrub else mysql_raw([defaults_file, system_database, '-e', text_of_sql].flatten.compact).scrub end elsif type.eql? 'regular' if File.file?("#{Facter.value(:root_home)}/.mylogin.cnf") ENV['MYSQL_TEST_LOGIN_FILE'] = "#{Facter.value(:root_home)}/.mylogin.cnf" mysql_raw(['-NBe', text_of_sql].flatten.compact).scrub else mysql_raw([defaults_file, '-NBe', text_of_sql].flatten.compact).scrub end else raise Puppet::Error, _("#mysql_caller: Unrecognised type '%{type}'" % { type: type }) end end def self.users mysql_caller("SELECT CONCAT(User, '@',Host) AS User FROM mysql.user", 'regular').split("\n") end # Optional parameter to run a statement on the MySQL system database. def self.system_database '--database=mysql' end def system_database self.class.system_database end # Take root@localhost and munge it to 'root'@'localhost' # Take root@id123@localhost and munge it to 'root@id123'@'localhost' def self.cmd_user(user) "'#{user.reverse.sub('@', "'@'").reverse}'" end # Take root.* and return ON `root`.* def self.cmd_table(table) table_string = '' # We can't escape *.* so special case this. table_string << if table == '*.*' '*.*' # Special case also for FUNCTIONs and PROCEDUREs elsif table.start_with?('FUNCTION ', 'PROCEDURE ') table.sub(%r{^(FUNCTION|PROCEDURE) (.*)(\..*)}, '\1 `\2`\3') else table.sub(%r{^(.*)(\..*)}, '`\1`\2') end table_string end def self.cmd_privs(privileges) return 'ALL PRIVILEGES' if privileges.include?('ALL') priv_string = '' privileges.each do |priv| priv_string << "#{priv}, " end # Remove trailing , from the last element. priv_string.sub(%r{, $}, '') end # Take in potential options and build up a query string with them. def self.cmd_options(options) option_string = '' options.each do |opt| option_string << ' WITH GRANT OPTION' if opt == 'GRANT' end option_string end end diff --git a/lib/puppet/provider/mysql_grant/mysql.rb b/lib/puppet/provider/mysql_grant/mysql.rb index 7899698..12999fe 100644 --- a/lib/puppet/provider/mysql_grant/mysql.rb +++ b/lib/puppet/provider/mysql_grant/mysql.rb @@ -1,178 +1,178 @@ # frozen_string_literal: true require File.expand_path(File.join(File.dirname(__FILE__), '..', 'mysql')) Puppet::Type.type(:mysql_grant).provide(:mysql, parent: Puppet::Provider::Mysql) do desc 'Set grants for users in MySQL.' commands mysql_raw: 'mysql' def self.instances instances = [] users.map do |user| user_string = cmd_user(user) query = "SHOW GRANTS FOR #{user_string};" begin grants = mysql_caller(query, 'regular') rescue Puppet::ExecutionFailure => e # Silently ignore users with no grants. Can happen e.g. if user is # defined with fqdn and server is run with skip-name-resolve. Example: # Default root user created by mysql_install_db on a host with fqdn # of myhost.mydomain.my: root@myhost.mydomain.my, when MySQL is started # with --skip-name-resolve. - next if e.inspect =~ %r{There is no such grant defined for user} + next if %r{There is no such grant defined for user}.match?(e.inspect) raise Puppet::Error, _('#mysql had an error -> %{inspect}') % { inspect: e.inspect } end # Once we have the list of grants generate entries for each. grants.each_line do |grant| # Match the munges we do in the type. munged_grant = grant.delete("'").delete('`').delete('"') # Matching: GRANT (SELECT, UPDATE) PRIVILEGES ON (*.*) TO ('root')@('127.0.0.1') (WITH GRANT OPTION) next unless match = munged_grant.match(%r{^GRANT\s(.+)\sON\s(.+)\sTO\s(.*)@(.*?)(\s.*)?$}) # rubocop:disable Lint/AssignmentInCondition privileges, table, user, host, rest = match.captures table.gsub!('\\\\', '\\') # split on ',' if it is not a non-'('-containing string followed by a # closing parenthesis ')'-char - e.g. only split comma separated elements not in # parentheses stripped_privileges = privileges.strip.split(%r{\s*,\s*(?![^(]*\))}).map do |priv| # split and sort the column_privileges in the parentheses and rejoin if priv.include?('(') type, col = priv.strip.split(%r{\s+|\b}, 2) type.upcase + ' (' + col.slice(1...-1).strip.split(%r{\s*,\s*}).sort.join(', ') + ')' else # Once we split privileges up on the , we need to make sure we # shortern ALL PRIVILEGES to just all. (priv == 'ALL PRIVILEGES') ? 'ALL' : priv.strip end end # Same here, but to remove OPTION leaving just GRANT. - options = if rest =~ %r{WITH\sGRANT\sOPTION} + options = if %r{WITH\sGRANT\sOPTION}.match?(rest) ['GRANT'] else ['NONE'] end # fix double backslash that MySQL prints, so resources match table.gsub!('\\\\', '\\') # We need to return an array of instances so capture these instances << new( name: "#{user}@#{host}/#{table}", ensure: :present, privileges: stripped_privileges.sort, table: table, user: "#{user}@#{host}", options: options, ) end end instances end def self.prefetch(resources) users = instances resources.keys.each do |name| if provider = users.find { |user| user.name == name } # rubocop:disable Lint/AssignmentInCondition resources[name].provider = provider end end end def grant(user, table, privileges, options) user_string = self.class.cmd_user(user) priv_string = self.class.cmd_privs(privileges) table_string = privileges.include?('PROXY') ? self.class.cmd_user(table) : self.class.cmd_table(table) query = "GRANT #{priv_string}" query << " ON #{table_string}" query << " TO #{user_string}" query << self.class.cmd_options(options) unless options.nil? self.class.mysql_caller(query, 'system') end def create grant(@resource[:user], @resource[:table], @resource[:privileges], @resource[:options]) @property_hash[:ensure] = :present @property_hash[:table] = @resource[:table] @property_hash[:user] = @resource[:user] @property_hash[:options] = @resource[:options] if @resource[:options] @property_hash[:privileges] = @resource[:privileges] exists? ? (return true) : (return false) end def revoke(user, table, revoke_privileges = ['ALL']) user_string = self.class.cmd_user(user) table_string = revoke_privileges.include?('PROXY') ? self.class.cmd_user(table) : self.class.cmd_table(table) priv_string = self.class.cmd_privs(revoke_privileges) # revoke grant option needs to be a extra query, because # "REVOKE ALL PRIVILEGES, GRANT OPTION [..]" is only valid mysql syntax # if no ON clause is used. # It hast to be executed before "REVOKE ALL [..]" since a GRANT has to # exist to be executed successfully if revoke_privileges.include?('ALL') && !revoke_privileges.include?('PROXY') query = "REVOKE GRANT OPTION ON #{table_string} FROM #{user_string}" self.class.mysql_caller(query, 'system') end query = "REVOKE #{priv_string} ON #{table_string} FROM #{user_string}" self.class.mysql_caller(query, 'system') end def destroy # if the user was dropped, it'll have been removed from the user hash # as the grants are already removed by the DROP statement if self.class.users.include? @property_hash[:user] if @property_hash[:privileges].include?('PROXY') revoke(@property_hash[:user], @property_hash[:table], @property_hash[:privileges]) else revoke(@property_hash[:user], @property_hash[:table]) end end @property_hash.clear exists? ? (return false) : (return true) end def exists? @property_hash[:ensure] == :present || false end def flush @property_hash.clear self.class.mysql_caller('FLUSH PRIVILEGES', 'regular') end mk_resource_methods def diff_privileges(privileges_old, privileges_new) diff = { revoke: [], grant: [] } if privileges_old.include? 'ALL' diff[:revoke] = privileges_old diff[:grant] = privileges_new elsif privileges_new.include? 'ALL' diff[:grant] = privileges_new else diff[:revoke] = privileges_old - privileges_new diff[:grant] = privileges_new - privileges_old end diff end def privileges=(privileges) diff = diff_privileges(@property_hash[:privileges], privileges) unless diff[:revoke].empty? revoke(@property_hash[:user], @property_hash[:table], diff[:revoke]) end unless diff[:grant].empty? grant(@property_hash[:user], @property_hash[:table], diff[:grant], @property_hash[:options]) end @property_hash[:privileges] = privileges self.privileges end def options=(options) revoke(@property_hash[:user], @property_hash[:table]) grant(@property_hash[:user], @property_hash[:table], @property_hash[:privileges], options) @property_hash[:options] = options self.options end end diff --git a/lib/puppet/provider/mysql_login_path/inifile.rb b/lib/puppet/provider/mysql_login_path/inifile.rb index 8261b2c..0ed5480 100644 --- a/lib/puppet/provider/mysql_login_path/inifile.rb +++ b/lib/puppet/provider/mysql_login_path/inifile.rb @@ -1,633 +1,633 @@ # encoding: UTF-8 # frozen_string_literal: true # See: https://github.com/puppetlabs/puppet/blob/main/lib/puppet/util/inifile.rb # This class represents the INI file and can be used to parse, modify, # and write INI files. class Puppet::Provider::MysqlLoginPath::IniFile < Puppet::Provider include Enumerable class Error < StandardError; end # VERSION = '3.0.0' # Public: Open an INI file and load the contents. # # filename - The name of the file as a String # opts - The Hash of options (default: {}) # :comment - String containing the comment character(s) # :parameter - String used to separate parameter and value # :encoding - Encoding String for reading / writing # :default - The String name of the default global section # # Examples # # IniFile.load('file.ini') # #=> IniFile instance # # IniFile.load('does/not/exist.ini') # #=> nil # # Returns an IniFile instance or nil if the file could not be opened. def self.load(filename, opts = {}) return unless File.file? filename new(opts.merge(filename: filename)) end # Get and set the filename attr_accessor :filename # Get and set the encoding attr_accessor :encoding # Public: Create a new INI file from the given set of options. If :content # is provided then it will be used to populate the INI file. If a :filename # is provided then the contents of the file will be parsed and stored in the # INI file. If neither the :content or :filename is provided then an empty # INI file is created. # # opts - The Hash of options (default: {}) # :content - The String/Hash containing the INI contents # :comment - String containing the comment character(s) # :parameter - String used to separate parameter and value # :encoding - Encoding String for reading / writing # :default - The String name of the default global section # :filename - The filename as a String # # Examples # # IniFile.new # #=> an empty IniFile instance # # IniFile.new( :content => "[global]\nfoo=bar" ) # #=> an IniFile instance # # IniFile.new( :filename => 'file.ini', :encoding => 'UTF-8' ) # #=> an IniFile instance # # IniFile.new( :content => "[global]\nfoo=bar", :comment => '#' ) # #=> an IniFile instance # def initialize(opts = {}) @comment = opts.fetch(:comment, ';#') @param = opts.fetch(:parameter, '=') @encoding = opts.fetch(:encoding, nil) @default = opts.fetch(:default, 'global') @filename = opts.fetch(:filename, nil) content = opts.fetch(:content, nil) @ini = Hash.new { |h, k| h[k] = {} } if content.is_a?(Hash) then merge!(content) elsif content then parse(content) elsif @filename then read end end # Public: Write the contents of this IniFile to the file system. If left # unspecified, the currently configured filename and encoding will be used. # Otherwise the filename and encoding can be specified in the options hash. # # opts - The default options Hash # :filename - The filename as a String # :encoding - The encoding as a String # # Returns this IniFile instance. def write(opts = {}) filename = opts.fetch(:filename, @filename) encoding = opts.fetch(:encoding, @encoding) mode = encoding ? "w:#{encoding}" : 'w' File.open(filename, mode) do |f| @ini.each do |section, hash| f.puts "[#{section}]" hash.each { |param, val| f.puts "#{param} #{@param} #{escape_value val}" } f.puts end end self end alias save write # Public: Read the contents of the INI file from the file system and replace # and set the state of this IniFile instance. If left unspecified the # currently configured filename and encoding will be used when reading from # the file system. Otherwise the filename and encoding can be specified in # the options hash. # # opts - The default options Hash # :filename - The filename as a String # :encoding - The encoding as a String # # Returns this IniFile instance if the read was successful; nil is returned # if the file could not be read. def read(opts = {}) filename = opts.fetch(:filename, @filename) encoding = opts.fetch(:encoding, @encoding) return unless File.file? filename mode = encoding ? "r:#{encoding}" : 'r' File.open(filename, mode) { |fd| parse fd } self end alias restore read # Returns this IniFile converted to a String. def to_s s = [] @ini.each do |section, hash| s << "[#{section}]" hash.each { |param, val| s << "#{param} #{@param} #{escape_value val}" } s << '' end s.join("\n") end # Returns this IniFile converted to a Hash. def to_h @ini.dup end # Public: Creates a copy of this inifile with the entries from the # other_inifile merged into the copy. # # other - The other IniFile. # # Returns a new IniFile. def merge(other) dup.merge!(other) end # Public: Merges other_inifile into this inifile, overwriting existing # entries. Useful for having a system inifile with user overridable settings # elsewhere. # # other - The other IniFile. # # Returns this IniFile. def merge!(other) return self if other.nil? my_keys = @ini.keys other_keys = case other when IniFile other.instance_variable_get(:@ini).keys when Hash other.keys else raise Error, "cannot merge contents from '#{other.class.name}'" end (my_keys & other_keys).each do |key| case other[key] when Hash @ini[key].merge!(other[key]) when nil nil else raise Error, "cannot merge section #{key.inspect} - unsupported type: #{other[key].class.name}" end end (other_keys - my_keys).each do |key| @ini[key] = case other[key] when Hash other[key].dup when nil {} else raise Error, "cannot merge section #{key.inspect} - unsupported type: #{other[key].class.name}" end end self end # Public: Yield each INI file section, parameter, and value in turn to the # given block. # # block - The block that will be iterated by the each method. The block will # be passed the current section and the parameter/value pair. # # Examples # # inifile.each do |section, parameter, value| # puts "#{parameter} = #{value} [in section - #{section}]" # end # # Returns this IniFile. def each return unless block_given? @ini.each do |section, hash| hash.each do |param, val| yield section, param, val end end self end # Public: Yield each section in turn to the given block. # # block - The block that will be iterated by the each method. The block will # be passed the current section as a Hash. # # Examples # # inifile.each_section do |section| # puts section.inspect # end # # Returns this IniFile. def each_section return unless block_given? @ini.each_key { |section| yield section } self end # Public: Remove a section identified by name from the IniFile. # # section - The section name as a String. # # Returns the deleted section Hash. def delete_section(section) @ini.delete section.to_s end # Public: Get the section Hash by name. If the section does not exist, then # it will be created. # # section - The section name as a String. # # Examples # # inifile['global'] # #=> global section Hash # # Returns the Hash of parameter/value pairs for this section. def [](section) return nil if section.nil? @ini[section.to_s] end # Public: Set the section to a hash of parameter/value pairs. # # section - The section name as a String. # value - The Hash of parameter/value pairs. # # Examples # # inifile['tenderloin'] = { 'gritty' => 'yes' } # #=> { 'gritty' => 'yes' } # # Returns the value Hash. def []=(section, value) @ini[section.to_s] = value end # Public: Create a Hash containing only those INI file sections whose names # match the given regular expression. # # regex - The Regexp used to match section names. # # Examples # # inifile.match(/^tree_/) # #=> Hash of matching sections # # Return a Hash containing only those sections that match the given regular # expression. def match(regex) @ini.dup.delete_if { |section, _| section !~ regex } end # Public: Check to see if the IniFile contains the section. # # section - The section name as a String. # # Returns true if the section exists in the IniFile. def section?(section) @ini.key? section.to_s end # Returns an Array of section names contained in this IniFile. def sections @ini.keys end # Public: Freeze the state of this IniFile object. Any attempts to change # the object will raise an error. # # Returns this IniFile. def freeze super @ini.each_value { |h| h.freeze } @ini.freeze self end # Public: Mark this IniFile as tainted -- this will traverse each section # marking each as tainted. # # Returns this IniFile. def taint super @ini.each_value { |h| h.taint } @ini.taint self end # Public: Produces a duplicate of this IniFile. The duplicate is independent # of the original -- i.e. the duplicate can be modified without changing the # original. The tainted state of the original is copied to the duplicate. # # Returns a new IniFile. def dup other = super other.instance_variable_set(:@ini, Hash.new { |h, k| h[k] = {} }) @ini.each_pair { |s, h| other[s].merge! h } other.taint if tainted? other end # Public: Produces a duplicate of this IniFile. The duplicate is independent # of the original -- i.e. the duplicate can be modified without changing the # original. The tainted state and the frozen state of the original is copied # to the duplicate. # # Returns a new IniFile. def clone other = dup other.freeze if frozen? other end # Public: Compare this IniFile to some other IniFile. For two INI files to # be equivalent, they must have the same sections with the same parameter / # value pairs in each section. # # other - The other IniFile. # # Returns true if the INI files are equivalent and false if they differ. def eql?(other) return true if equal? other return false unless other.instance_of? self.class @ini == other.instance_variable_get(:@ini) end alias == eql? # Escape special characters. # # value - The String value to escape. # # Returns the escaped value. def escape_value(value) value = value.to_s.dup value.gsub!(%r{\\([0nrt])}, '\\\\\1') value.gsub!(%r{\n}, '\n') value.gsub!(%r{\r}, '\r') value.gsub!(%r{\t}, '\t') value.gsub!(%r{\0}, '\0') value end # Parse the given content and store the information in this IniFile # instance. All data will be cleared out and replaced with the information # read from the content. # # content - A String or a file descriptor (must respond to `each_line`) # # Returns this IniFile. def parse(content) parser = Parser.new(@ini, @param, @comment, @default) parser.parse(content) self end # The IniFile::Parser has the responsibility of reading the contents of an # .ini file and storing that information into a ruby Hash. The object being # parsed must respond to `each_line` - this includes Strings and any IO # object. class Parser attr_writer :section attr_accessor :property attr_accessor :value # Create a new IniFile::Parser that can be used to parse the contents of # an .ini file. # # hash - The Hash where parsed information will be stored # param - String used to separate parameter and value # comment - String containing the comment character(s) # default - The String name of the default global section # def initialize(hash, param, comment, default) @hash = hash @default = default comment = comment.to_s.empty? ? '\\z' : "\\s*(?:[#{comment}].*)?\\z" @section_regexp = %r{\A\s*\[([^\]]+)\]#{comment}} @ignore_regexp = %r{\A#{comment}} @property_regexp = %r{\A(.*?)(? true # "false" --> false # "" --> nil # "42" --> 42 # "3.14" --> 3.14 # "foo" --> "foo" # # Returns the typecast value. def typecast(value) case value when %r{\Atrue\z}i then true when %r{\Afalse\z}i then false when %r{\A\s*\z}i then nil else begin begin Integer(value) - rescue - Float(value) + rescue + Float(value) end rescue unescape_value(value) end end end # Unescape special characters found in the value string. This will convert # escaped null, tab, carriage return, newline, and backslash into their # literal equivalents. # # value - The String value to unescape. # # Returns the unescaped value. def unescape_value(value) value = value.to_s value.gsub!(%r{\\[0nrt\\]}) do |char| case char when '\0' then "\0" when '\n' then "\n" when '\r' then "\r" when '\t' then "\t" when '\\\\' then '\\' end end value end end end # IniFile diff --git a/lib/puppet/provider/mysql_login_path/mysql_login_path.rb b/lib/puppet/provider/mysql_login_path/mysql_login_path.rb index 452aa74..a068fa4 100644 --- a/lib/puppet/provider/mysql_login_path/mysql_login_path.rb +++ b/lib/puppet/provider/mysql_login_path/mysql_login_path.rb @@ -1,166 +1,166 @@ # frozen_string_literal: true require File.expand_path(File.join(File.dirname(__FILE__), 'inifile')) require File.expand_path(File.join(File.dirname(__FILE__), 'sensitive')) require 'puppet/resource_api/simple_provider' require 'puppet/util/execution' require 'puppet/util/suidmanager' require 'open3' require 'pty' require 'expect' require 'fileutils' require 'English' # Implementation for the mysql_login_path type using the Resource API. class Puppet::Provider::MysqlLoginPath::MysqlLoginPath < Puppet::ResourceApi::SimpleProvider def get_homedir(_context, uid) result = Puppet::Util::Execution.execute(['/usr/bin/getent', 'passwd', uid], failonfail: true) result.split(':')[5] end def mysql_config_editor_set_cmd(context, uid, password = nil, *args) args.unshift('/usr/bin/mysql_config_editor') homedir = get_homedir(context, uid) login_file_path = "#{homedir}/.mylogin.cnf" if args.is_a?(Array) command = args.flatten.map(&:to_s) command_str = command.join(' ') elsif args.is_a?(String) command_str = command end begin Puppet::Util::SUIDManager.asuser(uid) do FileUtils.touch login_file_path FileUtils.chmod 0o600, login_file_path end PTY.spawn({ 'HOME' => homedir }, command_str) do |input, output, _pid| if password input.expect(%r{Enter password:}) output.puts password end end rescue => e raise Puppet::ExecutionFailure, _( "Execution of '%{str}' returned %{exit_status}: %{output}", ) % { str: command_str, exit_status: $CHILD_STATUS.exitstatus, output: e.message, } end end def mysql_config_editor_cmd(context, uid, *args) args.unshift('/usr/bin/mysql_config_editor') homedir = get_homedir(context, uid) Puppet::Util::Execution.execute( args, failonfail: true, uid: uid, custom_environment: { 'HOME' => homedir }, ) end def my_print_defaults_cmd(context, uid, *args) args.unshift('/usr/bin/my_print_defaults') homedir = get_homedir(context, uid) Puppet::Util::Execution.execute( args, failonfail: true, uid: uid, custom_environment: { 'HOME' => homedir }, ) end def get_password(context, uid, name) result = '' output = my_print_defaults_cmd(context, uid, '-s', name) output.split("\n").each do |line| - if line =~ %r{\-\-password} + if %r{\-\-password}.match?(line) result = line.sub(%r{\-\-password=}, '') end end result end def save_login_path(context, name, should) uid = name.fetch(:owner) args = ['set', '--skip-warn'] args.push('-G', should[:name].to_s) if should[:name] args.push('-h', should[:host].to_s) if should[:host] args.push('-u', should[:user].to_s) if should[:user] args.push('-S', should[:socket].to_s) if should[:socket] args.push('-P', should[:port].to_s) if should[:port] args.push('-p') if should[:password] && extract_pw(should[:password]) password = (should[:password] && extract_pw(should[:password])) ? extract_pw(should[:password]) : nil mysql_config_editor_set_cmd(context, uid, password, args) end def delete_login_path(context, name) login_path = name.fetch(:name) uid = name.fetch(:owner) mysql_config_editor_cmd(context, uid, 'remove', '-G', login_path) end def gen_pw(pw) Puppet::Provider::MysqlLoginPath::Sensitive.new(pw) end def extract_pw(sensitive) sensitive.unwrap end def list_login_paths(context, uid) result = [] output = mysql_config_editor_cmd(context, uid, 'print', '--all') ini = Puppet::Provider::MysqlLoginPath::IniFile.new(content: output) ini.each_section do |section| result.push(ensure: 'present', name: section, owner: uid.to_s, title: section + '-' + uid.to_s, host: ini[section]['host'].nil? ? nil : ini[section]['host'], user: ini[section]['user'].nil? ? nil : ini[section]['user'], password: ini[section]['password'].nil? ? nil : gen_pw(get_password(context, uid, section)), socket: ini[section]['socket'].nil? ? nil : ini[section]['socket'], port: ini[section]['port'].nil? ? nil : ini[section]['port']) end result end def get(context, name) result = [] owner = name.empty? ? ['root'] : name.map { |item| item[:owner] }.compact.uniq owner.each do |uid| login_paths = list_login_paths(context, uid) result += login_paths end result end def create(context, name, should) save_login_path(context, name, should) end def update(context, name, should) delete_login_path(context, name) save_login_path(context, name, should) end def delete(context, name) delete_login_path(context, name) end def canonicalize(_context, resources) resources.each do |r| if r.key?(:password) && r[:password].is_a?(Puppet::Pops::Types::PSensitiveType::Sensitive) r[:password] = gen_pw(extract_pw(r[:password])) end end end end diff --git a/lib/puppet/provider/mysql_user/mysql.rb b/lib/puppet/provider/mysql_user/mysql.rb index 25ebf45..5df9eae 100644 --- a/lib/puppet/provider/mysql_user/mysql.rb +++ b/lib/puppet/provider/mysql_user/mysql.rb @@ -1,257 +1,257 @@ # frozen_string_literal: true require File.expand_path(File.join(File.dirname(__FILE__), '..', 'mysql')) Puppet::Type.type(:mysql_user).provide(:mysql, parent: Puppet::Provider::Mysql) do desc 'manage users for a mysql database.' commands mysql_raw: 'mysql' # Build a property_hash containing all the discovered information about MySQL # users. def self.instances users = mysql_caller("SELECT CONCAT(User, '@',Host) AS User FROM mysql.user", 'regular').split("\n") # To reduce the number of calls to MySQL we collect all the properties in # one big swoop. users.map do |name| if mysqld_version.nil? ## Default ... # rubocop:disable Layout/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') 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, @authentication_string = mysql_caller(query, 'regular').chomp.split(%r{\t}) @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 Layout/LineLength new(name: name, ensure: :present, password_hash: @password, plugin: @plugin, 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, tls_options: @tls_options) end end # We iterate over each mysql_user entry in the catalog and compare it against # the contents of the property_hash generated by self.instances def self.prefetch(resources) users = instances # rubocop:disable Lint/AssignmentInCondition resources.keys.each do |name| if provider = users.find { |user| user.name == name } resources[name].provider = provider end end # rubocop:enable Lint/AssignmentInCondition end def create # (MODULES-3539) Allow @ in username merged_name = @resource[:name].reverse.sub('@', "'@'").reverse password_hash = @resource.value(:password_hash) plugin = @resource.value(:plugin) max_user_connections = @resource.value(:max_user_connections) || 0 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 if !plugin.nil? if !password_hash.nil? self.class.mysql_caller("CREATE USER '#{merged_name}' IDENTIFIED WITH '#{plugin}' AS '#{password_hash}'", 'system') else self.class.mysql_caller("CREATE USER '#{merged_name}' IDENTIFIED WITH '#{plugin}'", 'system') end @property_hash[:ensure] = :present @property_hash[:plugin] = plugin elsif newer_than('mysql' => '5.7.6', 'percona' => '5.7.6', 'mariadb' => '10.1.3') self.class.mysql_caller("CREATE USER IF NOT EXISTS '#{merged_name}' IDENTIFIED WITH 'mysql_native_password' AS '#{password_hash}'", 'system') @property_hash[:ensure] = :present @property_hash[:password_hash] = password_hash else self.class.mysql_caller("CREATE USER '#{merged_name}' IDENTIFIED BY PASSWORD '#{password_hash}'", 'system') @property_hash[:ensure] = :present @property_hash[:password_hash] = password_hash end # rubocop:disable Layout/LineLength if newer_than('mysql' => '5.7.6', 'percona' => '5.7.6') self.class.mysql_caller("ALTER USER IF EXISTS '#{merged_name}' WITH 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}", 'system') else self.class.mysql_caller("GRANT USAGE ON *.* TO '#{merged_name}' WITH 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}", 'system') end # rubocop:enable Layout/LineLength @property_hash[:max_user_connections] = max_user_connections @property_hash[:max_connections_per_hour] = max_connections_per_hour @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 newer_than('mysql' => '5.7.6', 'percona' => '5.7.6', 'mariadb' => '10.2.0') self.class.mysql_caller("ALTER USER '#{merged_name}' REQUIRE #{merged_tls_options}", 'system') else self.class.mysql_caller("GRANT USAGE ON *.* TO '#{merged_name}' REQUIRE #{merged_tls_options}", 'system') end @property_hash[:tls_options] = tls_options exists? ? (return true) : (return false) end def destroy # (MODULES-3539) Allow @ in username merged_name = @resource[:name].reverse.sub('@', "'@'").reverse if_exists = if newer_than('mysql' => '5.7', 'percona' => '5.7', 'mariadb' => '10.1.3') 'IF EXISTS ' else '' end self.class.mysql_caller("DROP USER #{if_exists}'#{merged_name}'", 'system') @property_hash.clear exists? ? (return false) : (return true) end def exists? @property_hash[:ensure] == :present || false end ## ## MySQL user properties ## # Generates method for all properties of the property_hash mk_resource_methods 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{^\*|^$} + raise ArgumentError, _('Only mysql_native_password (*ABCD...XXX) hashes are supported.') unless %r{^\*|^$}.match?(string) self.class.mysql_caller("ALTER USER #{merged_name} IDENTIFIED WITH mysql_native_password AS '#{string}'", 'system') else self.class.mysql_caller("SET PASSWORD FOR #{merged_name} = '#{string}'", 'system') end (password_hash == string) ? (return true) : (return false) end def max_user_connections=(int) merged_name = self.class.cmd_user(@resource[:name]) self.class.mysql_caller("GRANT USAGE ON *.* TO #{merged_name} WITH MAX_USER_CONNECTIONS #{int}", 'system').chomp (max_user_connections == int) ? (return true) : (return false) end def max_connections_per_hour=(int) merged_name = self.class.cmd_user(@resource[:name]) self.class.mysql_caller("GRANT USAGE ON *.* TO #{merged_name} WITH MAX_CONNECTIONS_PER_HOUR #{int}", 'system').chomp (max_connections_per_hour == int) ? (return true) : (return false) end def max_queries_per_hour=(int) merged_name = self.class.cmd_user(@resource[:name]) self.class.mysql_caller("GRANT USAGE ON *.* TO #{merged_name} WITH MAX_QUERIES_PER_HOUR #{int}", 'system').chomp (max_queries_per_hour == int) ? (return true) : (return false) end def max_updates_per_hour=(int) merged_name = self.class.cmd_user(@resource[:name]) self.class.mysql_caller("GRANT USAGE ON *.* TO #{merged_name} WITH MAX_UPDATES_PER_HOUR #{int}", 'system').chomp (max_updates_per_hour == int) ? (return true) : (return false) end def plugin=(string) merged_name = self.class.cmd_user(@resource[:name]) 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', 'mariadb' => '10.2.0') sql = "ALTER USER #{merged_name} IDENTIFIED WITH '#{string}'" sql << " AS '#{@resource[:password_hash]}'" if string == 'mysql_native_password' else # See https://bugs.mysql.com/bug.php?id=67449 sql = "UPDATE mysql.user SET plugin = '#{string}'" sql << ((string == 'mysql_native_password') ? ", password = '#{@resource[:password_hash]}'" : ", password = ''") sql << " WHERE CONCAT(user, '@', host) = '#{@resource[:name]}'" end self.class.mysql_caller(sql, 'system') (plugin == string) ? (return true) : (return false) end def tls_options=(array) merged_name = self.class.cmd_user(@resource[:name]) merged_tls_options = array.join(' AND ') if newer_than('mysql' => '5.7.6', 'percona' => '5.7.6', 'mariadb' => '10.2.0') self.class.mysql_caller("ALTER USER #{merged_name} REQUIRE #{merged_tls_options}", 'system') else self.class.mysql_caller("GRANT USAGE ON *.* TO #{merged_name} REQUIRE #{merged_tls_options}", 'system') 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' ['SSL'] elsif ssl_type == 'X509' ['X509'] elsif ssl_type == 'SPECIFIED' options = [] options << "CIPHER '#{ssl_cipher}'" if !ssl_cipher.nil? && !ssl_cipher.empty? options << "ISSUER '#{x509_issuer}'" if !x509_issuer.nil? && !x509_issuer.empty? options << "SUBJECT '#{x509_subject}'" if !x509_subject.nil? && !x509_subject.empty? options else ['NONE'] end end end diff --git a/lib/puppet/type/mysql_grant.rb b/lib/puppet/type/mysql_grant.rb index 45dab21..abaa332 100644 --- a/lib/puppet/type/mysql_grant.rb +++ b/lib/puppet/type/mysql_grant.rb @@ -1,121 +1,121 @@ # frozen_string_literal: true Puppet::Type.newtype(:mysql_grant) do @doc = <<-PUPPET @summary Manage a MySQL user's rights. PUPPET ensurable autorequire(:file) { '/root/.my.cnf' } autorequire(:mysql_user) { self[:user] } def initialize(*args) super # Forcibly munge any privilege with 'ALL' in the array to exist of just # 'ALL'. This can't be done in the munge in the property as that iterates # over the array and there's no way to replace the entire array before it's # returned to the provider. - if self[:ensure] == :present && Array(self[:privileges]).count > 1 && self[:privileges].to_s.include?('ALL') + if self[:ensure] == :present && Array(self[:privileges]).size > 1 && self[:privileges].to_s.include?('ALL') self[:privileges] = 'ALL' end # Sort the privileges array in order to ensure the comparision in the provider # self.instances method match. Otherwise this causes it to keep resetting the # privileges. # rubocop:disable Style/MultilineBlockChain self[:privileges] = Array(self[:privileges]).map { |priv| # split and sort the column_privileges in the parentheses and rejoin if priv.include?('(') type, col = priv.strip.split(%r{\s+|\b}, 2) type.upcase + ' (' + col.slice(1...-1).strip.split(%r{\s*,\s*}).sort.join(', ') + ')' else priv.strip.upcase end - }.uniq.reject { |k| k == 'GRANT' || k == 'GRANT OPTION' }.sort! + }.uniq.reject { |k| ['GRANT', 'GRANT OPTION'].include?(k) }.sort! end # rubocop:enable Style/MultilineBlockChain validate do raise(_('mysql_grant: `privileges` `parameter` is required.')) if self[:ensure] == :present && self[:privileges].nil? - raise(_('mysql_grant: `privileges` `parameter`: PROXY can only be specified by itself.')) if Array(self[:privileges]).count > 1 && Array(self[:privileges]).include?('PROXY') + raise(_('mysql_grant: `privileges` `parameter`: PROXY can only be specified by itself.')) if Array(self[:privileges]).size > 1 && Array(self[:privileges]).include?('PROXY') raise(_('mysql_grant: `table` `parameter` is required.')) if self[:ensure] == :present && self[:table].nil? raise(_('mysql_grant: `user` `parameter` is required.')) if self[:ensure] == :present && self[:user].nil? if self[:user] && self[:table] raise(_('mysql_grant: `name` `parameter` must match user@host/table format.')) if self[:name] != "#{self[:user]}/#{self[:table]}" end end newparam(:name, namevar: true) do desc 'Name to describe the grant.' munge do |value| value.delete("'") end end newproperty(:privileges, array_matching: :all) do desc 'Privileges for user' validate do |value| mysql_version = Facter.value(:mysql_version) if value =~ %r{proxy}i && Puppet::Util::Package.versioncmp(mysql_version, '5.5.0') < 0 raise(ArgumentError, _('mysql_grant: PROXY user not supported on mysql versions < 5.5.0. Current version %{version}.') % { version: mysql_version }) end end end newproperty(:table) do desc 'Table to apply privileges to.' validate do |value| if Array(@resource[:privileges]).include?('PROXY') && !%r{^[0-9a-zA-Z$_]*@[\w%\.:\-\/]*$}.match(value) raise(ArgumentError, _('mysql_grant: `table` `property` for PROXY should be specified as proxy_user@proxy_host.')) end end munge do |value| value.delete('`') end newvalues(%r{.*\..*}, %r{^[0-9a-zA-Z$_]*@[\w%\.:\-/]*$}) end newproperty(:user) do desc 'User to operate on.' validate do |value| # http://dev.mysql.com/doc/refman/5.5/en/identifiers.html # If at least one special char is used, string must be quoted # http://stackoverflow.com/questions/8055727/negating-a-backreference-in-regular-expressions/8057827#8057827 # rubocop:disable Lint/AssignmentInCondition # rubocop:disable Lint/UselessAssignment if matches = %r{^(['`"])((?!\1).)*\1@([\w%\.:\-/]+)$}.match(value) user_part = matches[2] host_part = matches[3] elsif matches = %r{^([0-9a-zA-Z$_]*)@([\w%\.:\-/]+)$}.match(value) user_part = matches[1] host_part = matches[2] elsif matches = %r{^((?!['`"]).*[^0-9a-zA-Z$_].*)@(.+)$}.match(value) user_part = matches[1] host_part = matches[2] else raise(ArgumentError, _('mysql_grant: Invalid database user %{user}.') % { user: value }) end # rubocop:enable Lint/AssignmentInCondition # rubocop:enable Lint/UselessAssignment mysql_version = Facter.value(:mysql_version) unless mysql_version.nil? raise(ArgumentError, _('mysql_grant: MySQL usernames are limited to a maximum of 16 characters.')) if Puppet::Util::Package.versioncmp(mysql_version, '5.7.8') < 0 && user_part.size > 16 raise(ArgumentError, _('mysql_grant: MySQL usernames are limited to a maximum of 32 characters.')) if Puppet::Util::Package.versioncmp(mysql_version, '10.0.0') < 0 && user_part.size > 32 raise(ArgumentError, _('mysql_grant: MySQL usernames are limited to a maximum of 80 characters.')) if Puppet::Util::Package.versioncmp(mysql_version, '10.0.0') > 0 && user_part.size > 80 end end munge do |value| matches = %r{^((['`"]?).*\2)@(.+)$}.match(value) "#{matches[1]}@#{matches[3].downcase}" end end newproperty(:options, array_matching: :all) do desc 'Options to grant.' end end diff --git a/spec/acceptance/types/mysql_plugin_spec.rb b/spec/acceptance/types/mysql_plugin_spec.rb index eed5910..7780241 100644 --- a/spec/acceptance/types/mysql_plugin_spec.rb +++ b/spec/acceptance/types/mysql_plugin_spec.rb @@ -1,65 +1,65 @@ # frozen_string_literal: true require 'spec_helper_acceptance' # Different operating systems (and therefore different versions/forks # of mysql) have varying levels of support for plugins and have # different plugins available. Choose a plugin that works or don't try # to test plugins if not available. if os[:family] == 'redhat' if os[:release].to_i == 5 plugin = nil # Plugins not supported on mysql on RHEL 5 elsif os[:release].to_i == 6 plugin = 'example' plugin_lib = 'ha_example.so' elsif os[:release].to_i == 7 plugin = 'pam' plugin_lib = 'auth_pam.so' end elsif os[:family] == 'debian' if os[:family] == 'ubuntu' - if os[:release] =~ %r{^16\.04|^18\.04} + if %r{^16\.04|^18\.04}.match?(os[:release]) # On Xenial running 5.7.12, the example plugin does not appear to be available. plugin = 'validate_password' plugin_lib = 'validate_password.so' else plugin = 'example' plugin_lib = 'ha_example.so' end end elsif os[:family] == 'suse' plugin = nil # Plugin library path is broken on Suse http://lists.opensuse.org/opensuse-bugs/2013-08/msg01123.html end describe 'mysql_plugin' do if plugin # if plugins are supported describe 'setup' do it 'works with no errors' do pp = <<-MANIFEST class { 'mysql::server': } MANIFEST apply_manifest(pp, catch_failures: true) end end describe 'load plugin' do pp = <<-MANIFEST mysql_plugin { #{plugin}: ensure => present, soname => '#{plugin_lib}', } MANIFEST it 'works without errors' do apply_manifest(pp, catch_failures: true) end it 'finds the plugin #stdout' do run_shell("mysql -NBe \"select plugin_name from information_schema.plugins where plugin_name='#{plugin}'\"") do |r| expect(r.stdout).to match(%r{^#{plugin}$}i) expect(r.stderr).to be_empty end end end end end diff --git a/spec/unit/puppet/provider/mysql_user/mysql_spec.rb b/spec/unit/puppet/provider/mysql_user/mysql_spec.rb index 624a844..2773d2e 100644 --- a/spec/unit/puppet/provider/mysql_user/mysql_spec.rb +++ b/spec/unit/puppet/provider/mysql_user/mysql_spec.rb @@ -1,497 +1,496 @@ # frozen_string_literal: true require 'spec_helper' describe Puppet::Type.type(:mysql_user).provider(:mysql) do # Output of mysqld -V mysql_version_string_hash = { 'mysql-5.5' => { version: '5.5.46', string: '/usr/sbin/mysqld Ver 5.5.46-log for Linux on x86_64 (MySQL Community Server (GPL))', mysql_type: 'mysql', }, 'mysql-5.6' => { version: '5.6.27', string: '/usr/sbin/mysqld Ver 5.6.27 for Linux on x86_64 (MySQL Community Server (GPL))', mysql_type: 'mysql', }, 'mysql-5.7.1' => { version: '5.7.1', string: '/usr/sbin/mysqld Ver 5.7.1 for Linux on x86_64 (MySQL Community Server (GPL))', mysql_type: 'mysql', }, 'mysql-5.7.6' => { version: '5.7.8', string: '/usr/sbin/mysqld Ver 5.7.8-rc for Linux on x86_64 (MySQL Community Server (GPL))', mysql_type: 'mysql', }, 'mariadb-10.0' => { version: '10.0.21', string: '/usr/sbin/mysqld Ver 10.0.21-MariaDB for Linux on x86_64 (MariaDB Server)', mysql_type: 'mariadb', }, 'mariadb-10.0-deb8' => { version: '10.0.23', 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', string: 'mysqld Ver 5.5.39-36.0-55 for Linux on x86_64 (Percona XtraDB Cluster (GPL), Release rel36.0, Revision 824, WSREP version 25.11, wsrep_25.11.r4023)', mysql_type: 'percona', }, } let(:defaults_file) { '--defaults-extra-file=/root/.my.cnf' } let(:system_database) { '--database=mysql' } let(:newhash) { '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5' } let(:raw_users) do <<-SQL_OUTPUT root@127.0.0.1 root@::1 @localhost debian-sys-maint@localhost root@localhost usvn_user@localhost @vagrant-ubuntu-raring-64 SQL_OUTPUT # rubocop:enable Layout/IndentHeredoc end let(:parsed_users) { ['root@127.0.0.1', 'root@::1', '@localhost', 'debian-sys-maint@localhost', 'root@localhost', 'usvn_user@localhost', '@vagrant-ubuntu-raring-64'] } let(:provider) { resource.provider } let(:instance) { provider.class.instances.first } let(:resource) do Puppet::Type.type(:mysql_user).new( ensure: :present, password_hash: '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4', name: 'joe@localhost', max_user_connections: '10', max_connections_per_hour: '10', max_queries_per_hour: '10', max_updates_per_hour: '10', provider: described_class.name, ) end before :each do # Set up the stubs for an instances call. Facter.stubs(:value).with(:root_home).returns('/root') Facter.stubs(:value).with(:mysql_version).returns('5.6.24') provider.class.instance_variable_set(:@mysqld_version_string, '5.6.24') Puppet::Util.stubs(:which).with('mysql').returns('/usr/bin/mysql') Puppet::Util.stubs(:which).with('mysqld').returns('/usr/sbin/mysqld') File.stubs(:file?).with('/root/.my.cnf').returns(true) provider.class.stubs(:mysql_caller).with("SELECT CONCAT(User, '@',Host) AS User FROM mysql.user", 'regular').returns('joe@localhost') 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 /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = 'joe@localhost'", 'regular').returns('10 10 10 10 *6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4') # rubocop:disable Layout/LineLength end describe 'self.instances' do it 'returns an array of users MySQL 5.5' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.5'][: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 /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'", 'regular').returns('10 10 10 10 ') } # rubocop:disable Layout/LineLength usernames = provider.class.instances.map { |x| x.name } expect(parsed_users).to match_array(usernames) end it 'returns an array of users MySQL 5.6' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.6'][: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 /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'", 'regular').returns('10 10 10 10 ') } # rubocop:disable Layout/LineLength usernames = provider.class.instances.map { |x| x.name } expect(parsed_users).to match_array(usernames) end it 'returns an array of users MySQL >= 5.7.0 < 5.7.6' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.1'][: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 /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'", 'regular').returns('10 10 10 10 ') } # rubocop:disable Layout/LineLength usernames = provider.class.instances.map { |x| x.name } expect(parsed_users).to match_array(usernames) end it 'returns an array of users MySQL >= 5.7.6' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.6'][: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, AUTHENTICATION_STRING, PLUGIN FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'", 'regular').returns('10 10 10 10 ') } # rubocop:disable Layout/LineLength usernames = provider.class.instances.map { |x| x.name } expect(parsed_users).to match_array(usernames) end it 'returns an array of users mariadb 10.0' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.0'][: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 /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'", 'regular').returns('10 10 10 10 ') } # rubocop:disable Layout/LineLength 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 Layout/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) 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 /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'", 'regular').returns('10 10 10 10 ') } # rubocop:disable Layout/LineLength usernames = provider.class.instances.map { |x| x.name } expect(parsed_users).to match_array(usernames) end end describe 'mysql version and type detection' do mysql_version_string_hash.each do |_name, line| version = line[:version] string = line[:string] mysql_type = line[:mysql_type] it "detects version '#{version}'" do provider.class.instance_variable_set(:@mysqld_version_string, string) expect(provider.mysqld_version).to eq(version) end it "detects type '#{mysql_type}'" do provider.class.instance_variable_set(:@mysqld_version_string, string) expect(provider.mysqld_type).to eq(mysql_type) end end end describe 'self.prefetch' do it 'exists' do provider.class.instances provider.class.prefetch({}) end end describe 'create' do it 'makes a user' do provider.class.expects(:mysql_caller).with("CREATE USER 'joe'@'localhost' IDENTIFIED BY PASSWORD '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4'", 'system') provider.class.expects(:mysql_caller).with("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", 'system') # rubocop:disable Layout/LineLength provider.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE NONE", 'system') provider.expects(:exists?).returns(true) expect(provider.create).to be_truthy end it 'creates a user using IF NOT EXISTS' do provider.class.instance_variable_set(:@mysqld_version_string, '5.7.6') - provider.class.expects(:mysql_caller).with("CREATE USER IF NOT EXISTS 'joe'@'localhost' IDENTIFIED WITH 'mysql_native_password' AS '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4'", 'system') # rubocop:disable Layout/LineLength - provider.class.expects(:mysql_caller).with("ALTER USER IF EXISTS 'joe'@'localhost' WITH MAX_USER_CONNECTIONS 10 MAX_CONNECTIONS_PER_HOUR 10 MAX_QUERIES_PER_HOUR 10 MAX_UPDATES_PER_HOUR 10", 'system') # rubocop:disable Layout/LineLength + provider.class.expects(:mysql_caller).with("CREATE USER IF NOT EXISTS 'joe'@'localhost' IDENTIFIED WITH 'mysql_native_password' AS '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4'", 'system') provider.class.expects(:mysql_caller).with("ALTER USER IF EXISTS 'joe'@'localhost' WITH MAX_USER_CONNECTIONS 10 MAX_CONNECTIONS_PER_HOUR 10 MAX_QUERIES_PER_HOUR 10 MAX_UPDATES_PER_HOUR 10", 'system') # rubocop:disable Layout/LineLength provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' REQUIRE NONE", 'system') provider.expects(:exists?).returns(true) expect(provider.create).to be_truthy end end describe 'destroy' do it 'removes a user if present' do provider.class.expects(:mysql_caller).with("DROP USER 'joe'@'localhost'", 'system') provider.expects(:exists?).returns(false) expect(provider.destroy).to be_truthy end it 'removes a user using IF EXISTS' do provider.class.instance_variable_set(:@mysqld_version_string, '5.7.1') provider.class.expects(:mysql_caller).with("DROP USER IF EXISTS 'joe'@'localhost'", 'system') expect(provider.destroy).to be_truthy end end describe 'exists?' do it 'checks if user exists' do expect(instance).to be_exists end end describe 'self.mysqld_version' do it 'uses the mysqld_version fact if unset' do provider.class.instance_variable_set(:@mysqld_version_string, nil) Facter.stubs(:value).with(:mysqld_version).returns('5.6.24') expect(provider.mysqld_version).to eq '5.6.24' end it 'returns 5.7.6 for "mysqld Ver 5.7.6 for Linux on x86_64 (MySQL Community Server (GPL))"' do provider.class.instance_variable_set(:@mysqld_version_string, 'mysqld Ver 5.7.6 for Linux on x86_64 (MySQL Community Server (GPL))') expect(provider.mysqld_version).to eq '5.7.6' end it 'returns 5.7.6 for "mysqld Ver 5.7.6-rc for Linux on x86_64 (MySQL Community Server (GPL))"' do provider.class.instance_variable_set(:@mysqld_version_string, 'mysqld Ver 5.7.6-rc for Linux on x86_64 (MySQL Community Server (GPL))') expect(provider.mysqld_version).to eq '5.7.6' end it 'detects >= 5.7.6 for 5.7.7-log' do provider.class.instance_variable_set(:@mysqld_version_string, 'mysqld Ver 5.7.7-log for Linux on x86_64 (MySQL Community Server (GPL))') expect(Puppet::Util::Package.versioncmp(provider.mysqld_version, '5.7.6')).to be >= 0 end it 'detects < 5.7.6 for 5.7.5-log' do provider.class.instance_variable_set(:@mysqld_version_string, 'mysqld Ver 5.7.5-log for Linux on x86_64 (MySQL Community Server (GPL))') expect(Puppet::Util::Package.versioncmp(provider.mysqld_version, '5.7.6')).to be < 0 end end describe 'self.defaults_file' do it 'sets --defaults-extra-file' do File.stubs(:file?).with('/root/.my.cnf').returns(true) expect(provider.defaults_file).to eq '--defaults-extra-file=/root/.my.cnf' end it 'fails if file missing' do File.expects(:file?).with('/root/.my.cnf').returns(false) expect(provider.defaults_file).to be_nil end end describe 'password_hash' do it 'returns a hash' do expect(instance.password_hash).to eq('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4') end end describe 'password_hash=' do it 'changes the hash mysql 5.5' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.5'][:string]) provider.class.expects(:mysql_caller).with("SET PASSWORD FOR 'joe'@'localhost' = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5'", 'system').returns('0') provider.expects(:password_hash).returns('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5') provider.password_hash = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5' end it 'changes the hash mysql 5.6' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.6'][:string]) provider.class.expects(:mysql_caller).with("SET PASSWORD FOR 'joe'@'localhost' = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5'", 'system').returns('0') provider.expects(:password_hash).returns('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5') provider.password_hash = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5' end it 'changes the hash mysql < 5.7.6' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.1'][:string]) provider.class.expects(:mysql_caller).with("SET PASSWORD FOR 'joe'@'localhost' = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5'", 'system').returns('0') provider.expects(:password_hash).returns('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5') provider.password_hash = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5' end it 'changes the hash MySQL >= 5.7.6' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.6'][:string]) - provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' IDENTIFIED WITH mysql_native_password AS '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5'", 'system').returns('0') # rubocop:disable Layout/LineLength + provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' IDENTIFIED WITH mysql_native_password AS '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5'", 'system').returns('0') provider.expects(:password_hash).returns('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5') provider.password_hash = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5' end it 'changes the hash mariadb-10.0' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.0'][:string]) provider.class.expects(:mysql_caller).with("SET PASSWORD FOR 'joe'@'localhost' = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5'", 'system').returns('0') 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 Layout/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 Layout/LineLength + provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' IDENTIFIED WITH ed25519 AS 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU'", 'system').returns('0') 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') provider.expects(:password_hash).returns('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5') provider.password_hash = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5' end end describe 'plugin=' do context 'auth_socket' do context 'MySQL < 5.7.6' do it 'changes the authentication plugin' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.1'][:string]) provider.class.expects(:mysql_caller).with("UPDATE mysql.user SET plugin = 'auth_socket', password = '' WHERE CONCAT(user, '@', host) = 'joe@localhost'", 'system').returns('0') provider.expects(:plugin).returns('auth_socket') provider.plugin = 'auth_socket' end end context 'MySQL >= 5.7.6' do it 'changes the authentication plugin' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.6'][:string]) provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' IDENTIFIED WITH 'auth_socket'", 'system').returns('0') provider.expects(:plugin).returns('auth_socket') provider.plugin = 'auth_socket' end end end context 'mysql_native_password' do context 'MySQL < 5.7.6' do it 'changes the authentication plugin' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.1'][:string]) provider.class.expects(:mysql_caller).with("UPDATE mysql.user SET plugin = 'mysql_native_password', password = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4' WHERE CONCAT(user, '@', host) = 'joe@localhost'", 'system').returns('0') # rubocop:disable Layout/LineLength provider.expects(:plugin).returns('mysql_native_password') provider.plugin = 'mysql_native_password' end end context 'MySQL >= 5.7.6' do it 'changes the authentication plugin' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.6'][:string]) - provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' IDENTIFIED WITH 'mysql_native_password' AS '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4'", 'system').returns('0') # rubocop:disable Layout/LineLength + provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' IDENTIFIED WITH 'mysql_native_password' AS '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4'", 'system').returns('0') provider.expects(:plugin).returns('mysql_native_password') provider.plugin = 'mysql_native_password' 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 Layout/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 Layout/LineLength + provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' IDENTIFIED WITH 'ed25519' AS 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU'", 'system').returns('0') provider.expects(:plugin).returns('ed25519') provider.plugin = 'ed25519' end end end # rubocop:enable RSpec/NestedGroups 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.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE NONE", 'system').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.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE NONE", 'system').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.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE NONE", 'system').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.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' REQUIRE NONE", 'system').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.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE NONE", 'system').returns('0') provider.expects(:tls_options).returns(['NONE']) provider.tls_options = ['NONE'] end end describe 'tls_options=required' do it 'adds mTLS option grant in mysql 5.5' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.5'][:string]) provider.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE ISSUER '/CN=Certificate Authority' AND SUBJECT '/OU=MySQL Users/CN=Username'", 'system').returns('0') provider.expects(:tls_options).returns(['ISSUER \'/CN=Certificate Authority\'', 'SUBJECT \'/OU=MySQL Users/CN=Username\'']) provider.tls_options = ['ISSUER \'/CN=Certificate Authority\'', 'SUBJECT \'/OU=MySQL Users/CN=Username\''] end it 'adds mTLS option grant in mysql 5.6' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.6'][:string]) provider.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE ISSUER '/CN=Certificate Authority' AND SUBJECT '/OU=MySQL Users/CN=Username'", 'system').returns('0') provider.expects(:tls_options).returns(['ISSUER \'/CN=Certificate Authority\'', 'SUBJECT \'/OU=MySQL Users/CN=Username\'']) provider.tls_options = ['ISSUER \'/CN=Certificate Authority\'', 'SUBJECT \'/OU=MySQL Users/CN=Username\''] end it 'adds mTLS 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.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE ISSUER '/CN=Certificate Authority' AND SUBJECT '/OU=MySQL Users/CN=Username'", 'system').returns('0') provider.expects(:tls_options).returns(['ISSUER \'/CN=Certificate Authority\'', 'SUBJECT \'/OU=MySQL Users/CN=Username\'']) provider.tls_options = ['ISSUER \'/CN=Certificate Authority\'', 'SUBJECT \'/OU=MySQL Users/CN=Username\''] end it 'adds mTLS 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.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' REQUIRE ISSUER '/CN=Certificate Authority' AND SUBJECT '/OU=MySQL Users/CN=Username'", 'system').returns('0') provider.expects(:tls_options).returns(['ISSUER \'/CN=Certificate Authority\'', 'SUBJECT \'/OU=MySQL Users/CN=Username\'']) provider.tls_options = ['ISSUER \'/CN=Certificate Authority\'', 'SUBJECT \'/OU=MySQL Users/CN=Username\''] end it 'adds mTLS option grant in mariadb-10.0' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.0'][:string]) provider.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE ISSUER '/CN=Certificate Authority' AND SUBJECT '/OU=MySQL Users/CN=Username'", 'system').returns('0') provider.expects(:tls_options).returns(['ISSUER \'/CN=Certificate Authority\'', 'SUBJECT \'/OU=MySQL Users/CN=Username\'']) provider.tls_options = ['ISSUER \'/CN=Certificate Authority\'', 'SUBJECT \'/OU=MySQL Users/CN=Username\''] end end ['max_user_connections', 'max_connections_per_hour', 'max_queries_per_hour', 'max_updates_per_hour'].each do |property| describe property do it "returns #{property}" do expect(instance.send(property.to_s.to_sym)).to eq('10') end end describe "#{property}=" do it "changes #{property}" do provider.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' WITH #{property.upcase} 42", 'system').returns('0') provider.expects(property.to_sym).returns('42') provider.send("#{property}=".to_sym, '42') end end end end