diff --git a/.fixtures.yml b/.fixtures.yml index 82ec2d27..a5f7e87f 100644 --- a/.fixtures.yml +++ b/.fixtures.yml @@ -2,5 +2,6 @@ fixtures: forge_modules: acl: "puppetlabs/acl" stdlib: "puppetlabs/stdlib" + acl: "puppetlabs/acl" symlinks: - "sqlserver": "#{source_dir}" + sqlserver: "#{source_dir}" diff --git a/.travis.yml b/.travis.yml index 7abb0631..7cfecdf8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,12 @@ --- +sudo: false language: ruby bundler_args: --without development script: "bundle exec rake spec SPEC_OPTS='--format documentation'" rvm: - 1.9.3 - 2.0.0 + - 2.1.5 env: matrix: - PUPPET_GEM_VERSION="~> 3.7.1" diff --git a/README.md b/README.md index b75b2cd9..6c6e4196 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,39 @@ sqlserver::login{'WIN-D95P1A3V103\localAccount': } ``` +###To run custom TSQL statements: + +To use `sqlserver_tsql` to trigger other classes or defined types: + +``` +sqlserver_tsql{ 'Query Logging DB Status': + instance => 'MSSQLSERVER', + onlyif => "IF (SELECT count(*) FROM myDb.dbo.logging_table WHERE + message like 'FATAL%') > 1000 THROW 50000, 'Fatal Exceptions in Logging', 10", + notify => Exec['Too Many Fatal Errors'] +} +``` + +To clean up regular logs with conditional checks: + +``` +sqlserver_tsql{ 'Cleanup Old Logs': + instance => 'MSSQLSERVER', + command => "DELETE FROM myDb.dbo.logging_table WHERE log_date < '${log_max_date}'", + onlyif => "IF exists(SELECT * FROM myDb.dbo.logging_table WHERE log_date < '${log_max_date}') + THROW 50000, 'need log cleanup', 10", +} +``` + +If you want something to always execute, you can leave out the `onlyif` parameter: + +``` +sqlserver_tsql{ 'Always running': + instance => 'MSSQLSERVER', + command => 'EXEC notified_executor()', +} +``` + #### Windows SQL Server Terms Terminology differs somewhat between various database systems; please refer to this list of terms for clarification. diff --git a/lib/puppet/parser/functions/sqlserver_upcase.rb b/lib/puppet/parser/functions/sqlserver_upcase.rb new file mode 100644 index 00000000..b51405d4 --- /dev/null +++ b/lib/puppet/parser/functions/sqlserver_upcase.rb @@ -0,0 +1,28 @@ +module Puppet::Parser::Functions + newfunction(:sqlserver_upcase, :type => :rvalue, :arity => 1) do |arguments| + + raise(Puppet::ParseError, "upcase(): Wrong number of arguments " + + "given (#{arguments.size} for 1)") if arguments.size != 1 + + value = arguments[0] + + unless value.is_a?(Array) || value.is_a?(Hash) || value.respond_to?(:upcase) + raise(Puppet::ParseError, 'upcase(): Requires an ' + + 'array, hash or object that responds to upcase in order to work') + end + + if value.is_a?(Array) + # Numbers in Puppet are often string-encoded which is troublesome ... + result = value.collect { |i| function_sqlserver_upcase([i]) } + elsif value.is_a?(Hash) + result = {} + value.each_pair do |k, v| + result[function_sqlserver_upcase([k])] = function_sqlserver_upcase([v]) + end + else + result = value.upcase + end + + return result + end +end diff --git a/lib/puppet/parser/functions/sqlserver_validate_hash_uniq_values.rb b/lib/puppet/parser/functions/sqlserver_validate_hash_uniq_values.rb new file mode 100644 index 00000000..76c9cf81 --- /dev/null +++ b/lib/puppet/parser/functions/sqlserver_validate_hash_uniq_values.rb @@ -0,0 +1,21 @@ +# === Defined Parser Function: sqlserver_validate_hash_uniq_values +# +# [args*] A hash, that contains string or string[] for values +# +# @raise [Puppet::ParserError] When duplicates are found +# +module Puppet::Parser::Functions + newfunction(:sqlserver_validate_hash_uniq_values) do |arguments| + + raise(Puppet::ParseError, 'Expect a Hash as an argument') unless arguments[0].is_a?(Hash) + + value = arguments[0].each_value.collect { |v| v }.flatten + + total_count = value.count + uniq_count = value.uniq.count + msg = arguments[1] ? arguments[1] : "Duplicate values passed to hash #{value}" + if uniq_count != total_count + raise(Puppet::ParseError, msg) + end + end +end diff --git a/lib/puppet/parser/functions/sqlserver_validate_instance_name.rb b/lib/puppet/parser/functions/sqlserver_validate_instance_name.rb index 74991c77..152876e2 100644 --- a/lib/puppet/parser/functions/sqlserver_validate_instance_name.rb +++ b/lib/puppet/parser/functions/sqlserver_validate_instance_name.rb @@ -20,6 +20,9 @@ module Puppet::Parser::Functions end value = args[0] errors = [] + if value.length < 1 || value.empty? + errors << "Instance name must be between 1 to 16 characters" + end if value.length > 16 errors << "Instance name can not be larger than 16 characters, you provided #{value}" end diff --git a/lib/puppet/parser/functions/sqlserver_validate_range.rb b/lib/puppet/parser/functions/sqlserver_validate_range.rb index a507b086..c70f133b 100644 --- a/lib/puppet/parser/functions/sqlserver_validate_range.rb +++ b/lib/puppet/parser/functions/sqlserver_validate_range.rb @@ -3,15 +3,21 @@ module Puppet::Parser::Functions if (args.length < 3) or (args.length > 4) then raise Puppet::ParseError, ("validate_range(): wrong number of arguments (#{args.length}; must be 3)") end - value, lower, upper, msg = args + values, lower, upper, msg = args - msg = msg || "validate_range(): #{args[0].inspect} is not between #{args[1].inspect} and #{args[2].inspect}" - if /^\d+(|\.\d+)$/.match(value) - raise(Puppet::ParseError, msg) unless Float(value).between?(Float(lower), Float(upper)) - else - value.strip! - raise(Puppet::ParseError, msg) unless value.length >= Integer(lower) && value.length <= Integer(upper) + if values.is_a? String + values = Array.new << values + end + + values.each do |value| + msg = msg || "validate_range(): #{args[0].inspect} is not between #{args[1].inspect} and #{args[2].inspect}" + if /^\d+(|\.\d+)$/.match(value) + raise(Puppet::ParseError, msg) unless Float(value).between?(Float(lower), Float(upper)) + else + value.strip! + raise(Puppet::ParseError, msg) unless value.length >= Integer(lower) && value.length <= Integer(upper) + end end end end diff --git a/lib/puppet/property/login.rb b/lib/puppet/property/sqlserver_login.rb similarity index 100% rename from lib/puppet/property/login.rb rename to lib/puppet/property/sqlserver_login.rb diff --git a/lib/puppet/property/sqlserver_tsql.rb b/lib/puppet/property/sqlserver_tsql.rb new file mode 100644 index 00000000..0e68b838 --- /dev/null +++ b/lib/puppet/property/sqlserver_tsql.rb @@ -0,0 +1,19 @@ +require 'puppet/property' + +class Puppet::Property::SqlserverTsql < Puppet::Property + desc 'TSQL property that we are going to wrap with a try catch' + munge do |value| + erb_template = <<-TEMPLATE +BEGIN TRY + #{value} +END TRY +BEGIN CATCH + DECLARE @msg as VARCHAR(max); + SELECT @msg = 'THROW CAUGHT: ' + ERROR_MESSAGE(); + THROW 51000, @msg, 10 +END CATCH + TEMPLATE + value = erb_template + end + +end diff --git a/lib/puppet/provider/sqlserver.rb b/lib/puppet/provider/sqlserver.rb index 213079d7..eabb84be 100644 --- a/lib/puppet/provider/sqlserver.rb +++ b/lib/puppet/provider/sqlserver.rb @@ -51,8 +51,15 @@ def self.run_authenticated_sqlcmd(query, opts) temp_ps1.write(ps1.result(b)) temp_ps1.flush temp_ps1.close + #We want to not fail the exec but fail the overall process once we get the clean result back, otherwise we report the temp file which is meaningless result = Puppet::Util::Execution.execute(['powershell.exe', '-noprofile', '-executionpolicy', 'unrestricted', temp_ps1.path], {:failonfail => false}) #We expect some things to fail in order to run as an only if - debug("Return result #{result.exitstatus}") + debug("Return result #{result}") + if opts[:failonfail] && result.match(/THROW CAUGHT/) + fail(result.gsub('THROW CAUGHT:', '')) + end + if result.match(/Msg \d+, Level 16/) + fail(result) + end return result ensure temp_ps1.close diff --git a/lib/puppet/provider/sqlserver_tsql/mssql.rb b/lib/puppet/provider/sqlserver_tsql/mssql.rb index f7fafbc1..9582db14 100644 --- a/lib/puppet/provider/sqlserver_tsql/mssql.rb +++ b/lib/puppet/provider/sqlserver_tsql/mssql.rb @@ -1,21 +1,61 @@ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'sqlserver')) +require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'puppet_x/sqlserver/sql_connection')) Puppet::Type::type(:sqlserver_tsql).provide(:mssql, :parent => Puppet::Provider::Sqlserver) do + def run(query) debug("Running resource #{query} against #{resource[:instance]}") - result = Puppet::Provider::Sqlserver.run_authenticated_sqlcmd(query, {:instance_name => resource[:instance]}) - return result + config = get_instance_config + begin + sqlconn = PuppetX::Sqlserver::SqlConnection.new + sqlconn.open(config["admin"], config["pass"], resource[:instance]) + sqlconn.command(query) + result = ResultOutput.new(sqlconn) + if result.has_errors + debug("Returned a result of #{result.exitstatus}, with #{parse_for_error(result.error_message)}") + end + return result + ensure + sqlconn.close + end + end + + def get_instance_config + config_file = File.join(Puppet[:vardir], "cache/sqlserver/.#{resource[:instance]}.cfg") + if !File.exists? (config_file) + fail('Required config file missing, add the appropriate sqlserver::config and rerun') + end + if !File.readable?(config_file) + fail('Unable to read config file, ensure proper permissions and please try again') + end + JSON.parse(File.read(config_file)) end def run_check - result = self.run(resource[:onlyif]) - return result + return self.run(resource[:onlyif]) end def run_update - result = self.run(resource[:command]) - return result + return self.run(resource[:command]) + end + def parse_for_error(result) + match = result.match(/SQL Server\n\s+(.*)/i) + match[1] unless match == nil + end + + class ResultOutput + attr_reader :exitstatus, :error_message + + def initialize(sqlconn) + @exitstatus = sqlconn.has_errors ? 1 : 0 + @error_message = sqlconn.error_message + end + + def has_errors + @exitstatus != 0 + end + end end diff --git a/lib/puppet/templates/authenticated_query.ps1.erb b/lib/puppet/templates/authenticated_query.ps1.erb index 92f032ea..a66b62ce 100644 --- a/lib/puppet/templates/authenticated_query.ps1.erb +++ b/lib/puppet/templates/authenticated_query.ps1.erb @@ -24,14 +24,22 @@ if (!(Get-Command 'sqlcmd.exe' -ErrorAction SilentlyContinue)){ } $result = sqlcmd.exe -i '<%= input_file %>' -h-1 -W -s ',' <% if @instance != 'MSSQLSERVER' %>-S localhost\<%= @instance %><%end%> - if($result -match "ERROR"){ - Write-Error -Message ($result | where {$_ -match "ERROR"} | select -First 1) + if($result -match "THROW CAUGHT"){ + Write-Host ($result | where {$_ -match "THROW CAUGHT"} | select -First 1) + Write-Error -Message ($result | where {$_ -match "THROW CAUGHT"} | select -First 1) exit(10) } if($result -match "Incorrect syntax near "){ + Write-Host ($result | where {$_ -match "Incorrect syntax near"} | select -First 1) Write-Error -Message ($result | where {$_ -match "Incorrect syntax"} | select -First 1) exit(10) } + if($result -match "Msg \d+, Level 16"){ + $msg = $result -join ' ' + Write-Host $msg + Write-Error -Message "ERROR: $msg" + exit(10) + } } catch{ Write-Host $_ diff --git a/lib/puppet/type/sqlserver_features.rb b/lib/puppet/type/sqlserver_features.rb index 508009be..55a8edfb 100644 --- a/lib/puppet/type/sqlserver_features.rb +++ b/lib/puppet/type/sqlserver_features.rb @@ -1,4 +1,4 @@ -require File.expand_path(File.join(File.dirname(__FILE__), '..', 'property/login')) +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'property/sqlserver_login')) require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'puppet_x/sqlserver/server_helper')) Puppet::Type::newtype(:sqlserver_features) do diff --git a/lib/puppet/type/sqlserver_instance.rb b/lib/puppet/type/sqlserver_instance.rb index fc5f0be7..7ca7bfbb 100644 --- a/lib/puppet/type/sqlserver_instance.rb +++ b/lib/puppet/type/sqlserver_instance.rb @@ -1,4 +1,4 @@ -require File.expand_path(File.join(File.dirname(__FILE__), '..', 'property/login')) +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'property/sqlserver_login')) require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'puppet_x/sqlserver/server_helper')) Puppet::Type::newtype(:sqlserver_instance) do diff --git a/lib/puppet/type/sqlserver_tsql.rb b/lib/puppet/type/sqlserver_tsql.rb index 91873d81..45c67930 100644 --- a/lib/puppet/type/sqlserver_tsql.rb +++ b/lib/puppet/type/sqlserver_tsql.rb @@ -1,4 +1,5 @@ require 'puppet' +require 'puppet/property/sqlserver_tsql' Puppet::Type::newtype(:sqlserver_tsql) do newparam :name, :namevar => true @@ -15,21 +16,23 @@ def self.checks end desc 'command to run against an instance with the authenticated credentials used in sqlserver::config' - newparam(:command) do + newparam(:command, :parent => Puppet::Property::SqlserverTsql) do end desc 'requires the usage of sqlserver::config with the user and password' newparam(:instance) do - + munge do |value| + value.upcase + end end desc 'SQL Query to run and only run if exits with non-zero' - newcheck(:onlyif) do - # Return true if the command returns 0. + newcheck(:onlyif, :parent => Puppet::Property::SqlserverTsql) do + #Runs in the event that our TSQL exits with anything other than 0 def check(value) begin - output = provider.run(value) + output = provider.run(value, :failonfail => false) end debug("OnlyIf returned exitstatus of #{output.exitstatus}") output.exitstatus != 0 @@ -58,7 +61,10 @@ def output def refresh if self.check_all_attributes(true) - provider.run_update + result = provider.run_update + if result.has_errors + fail("Unable to apply changes, failed with error message #{result.error_message}") + end end end diff --git a/lib/puppet_x/sqlserver/sql_connection.rb b/lib/puppet_x/sqlserver/sql_connection.rb new file mode 100644 index 00000000..4ffc1601 --- /dev/null +++ b/lib/puppet_x/sqlserver/sql_connection.rb @@ -0,0 +1,107 @@ +module PuppetX + module Sqlserver + class SqlConnection + attr_reader :data, :fields, :exception_caught + + def initialize + @_connection = nil + @data = nil + end + + def connection + @_connection ||= create_connection + end + + + def open(user, pass, instance, database = 'master') + # Open ADO connection to the SQL Server database + connection_string = "Provider=SQLOLEDB.1;" + connection_string << "Persist Security Info=False;" + connection_string << "User ID=#{user};" + connection_string << "password=#{pass};" + connection_string << "Initial Catalog=#{database};" + if instance !~ /^MSSQLSERVER$/ + connection_string << "Data Source=localhost\\#{instance};" + else + connection_string << "Data Source=localhost;" + end + connection_string << "Network Library=dbmssocn" + connection.Open(connection_string) + end + + + def command(sql) + clear_previous + begin + r = execute(sql) + yield(r) if block_given? + rescue sql_exception_class => e + @exception_caught = e + end + nil + end + + ## + # @param String sql a query that results in rows returned + # @return Array[Hash] Returns an array of rows, as hash values, with the column name as keys for each row + ## + def fetch_rows(sql) + rows = [] + begin + command(sql) do |result| + cols = parse_column_names result + result.getRows.transpose.each do |r| + row = {} + cols.each { |c| row[c] = r.shift } + rows << row + end unless result.eof + end + rescue sql_exception_class => e + @exception_caught = e + end + rows + end + + def has_errors + return @exception_caught != nil + end + + def error_message + @exception_caught.message unless @exception_caught == nil + end + + def close + connection.Close + end + + private + def clear_previous + @data = nil + @fields = nil + @exception_caught = nil + end + + def create_connection + require 'win32ole' + connection = WIN32OLE.new('ADODB.Connection') + end + + def execute (sql) + connection.Execute(sql, nil, nil) + end + + def parse_column_names(result) + result.Fields.extend(Enumerable).map { |column| column.Name } + end + + # having as a method instead of hard coded allows us to stub and test outside of Windows + def sql_exception_class + ::WIN32OLERuntimeError + end + + def connection=(conn) + @_connection = conn + end + end + end +end diff --git a/manifests/config.pp b/manifests/config.pp index 640782fa..28f26949 100644 --- a/manifests/config.pp +++ b/manifests/config.pp @@ -24,14 +24,10 @@ $instance_name = $title, ) { #possible future parameter if we do end up supporting different install directories - $install_dir ='C:/Program Files/Microsoft SQL Server' - $config_dir = "${install_dir}/.puppet" + $config_dir = "${::puppet_vardir}/cache/sqlserver" $config_file = "${config_dir}/.${instance_name}.cfg" - if !defined(File[$config_dir]){ - file{ $config_dir: - ensure => directory - } - } + ensure_resource('file', ["${::puppet_vardir}/cache",$config_dir], { 'ensure' => 'directory','recurse' => 'true' }) + file{ $config_file: content => template('sqlserver/instance_config.erb'), require => File[$config_dir], diff --git a/manifests/login.pp b/manifests/login.pp index a05c98e4..e712902f 100644 --- a/manifests/login.pp +++ b/manifests/login.pp @@ -45,6 +45,10 @@ # @see http://technet.microsoft.com/en-us/library/ms189751(v=sql.110).aspx Create Login # @see http://technet.microsoft.com/en-us/library/ms189828(v=sql.110).aspx Alter Login # +# [permissions] +# A hash of permissions that should be managed for the login. Valid keys are 'GRANT', 'GRANT_WITH_OPTION', 'DENY' or 'REVOKE'. Valid values must be an array of Strings i.e. {'GRANT' => ['CONNECT SQL', 'CREATE ANY DATABASE'] } +# +## define sqlserver::login ( $login = $title, $instance = 'MSSQLSERVER', @@ -57,6 +61,7 @@ $check_expiration = false, $check_policy = true, $disabled = false, + $permissions = { }, ) { sqlserver_validate_instance_name($instance) @@ -67,15 +72,52 @@ fail ('Can not have check expiration enabled when check_policy is disabled') } - $create_delete = $ensure ? { + $_create_delete = $ensure ? { present => 'create', absent => 'delete', } sqlserver_tsql{ "login-${instance}-${login}": instance => $instance, - command => template("sqlserver/${create_delete}/login.sql.erb"), + command => template("sqlserver/${_create_delete}/login.sql.erb"), onlyif => template('sqlserver/query/login_exists.sql.erb'), require => Sqlserver::Config[$instance] } + + if $ensure == present { + validate_hash($permissions) + $_upermissions = sqlserver_upcase($permissions) + sqlserver_validate_hash_uniq_values($_upermissions, "Duplicate permissions found for sqlserver::login[${title}]") + + Sqlserver::Login::Permissions{ + login => $login, + instance => $instance, + require => Sqlserver_tsql["login-${instance}-${login}"] + } + if has_key($_upermissions, 'GRANT') and is_array($_upermissions['GRANT']) { + sqlserver::login::permissions{ "Sqlserver::Login[${title}]-GRANT-${login}": + state => 'GRANT', + permissions => $_upermissions['GRANT'], + } + } + if has_key($_upermissions, 'DENY') and is_array($_upermissions['DENY']) { + sqlserver::login::permissions{ "Sqlserver::Login[${title}]-DENY-${login}": + state => 'DENY', + permissions => $_upermissions['DENY'], + } + } + if has_key($_upermissions, 'REVOKE') and is_array($_upermissions['REVOKE']) { + sqlserver::login::permissions{ "Sqlserver::Login[${title}]-REVOKE-${login}": + state => 'REVOKE', + permissions => $_upermissions['REVOKE'], + } + } + if has_key($_upermissions, 'GRANT_WITH_OPTION') and is_array($_upermissions['GRANT_WITH_OPTION']) { + sqlserver::login::permissions{ "Sqlserver::Login[${title}]-GRANT-WITH_GRANT_OPTION-${login}": + state => 'GRANT', + with_grant_option => true, + permissions => $_upermissions['GRANT_WITH_OPTION'], + } + } + } } diff --git a/manifests/login/permissions.pp b/manifests/login/permissions.pp new file mode 100644 index 00000000..441acd1a --- /dev/null +++ b/manifests/login/permissions.pp @@ -0,0 +1,52 @@ +## +# == Define Resource Type: sqlserver::login::permissions# +# +# === Requirement/Dependencies: +# +# Requires defined type {sqlserver::config} in order to execute against the SQL Server instance +# +# +# === Parameters +# [login] +# The login for which the permission will be manage. +# +# [permissions] +# An array of permissions you would like managed. i.e. ['SELECT', 'INSERT', 'UPDATE', 'DELETE'] +# +# [state] +# The state you would like the permission in. Accepts 'GRANT', 'DENY', 'REVOKE' Please note that REVOKE equates to absent and will default to database and system level permissions. +# +# [instance] +# The name of the instance where the user and database exists. Defaults to 'MSSQLSERVER' +# +## +define sqlserver::login::permissions ( + $login, + $permissions, + $state = 'GRANT', + $with_grant_option = false, + $instance = 'MSSQLSERVER', +){ + sqlserver_validate_instance_name($instance) + +## Validate Permissions + sqlserver_validate_range($permissions, 4, 128, 'Permission must be between 4 and 128 characters') + validate_array($permissions) + + sqlserver_validate_range($login, 1, 128, 'Login must be between 1 and 128 characters') + +## Validate state + $_state = upcase($state) + validate_re($_state,'^(GRANT|REVOKE|DENY)$', "State parameter can only be one of 'GRANT', 'REVOKE' or 'DENY', you passed a value of ${state}") + + validate_bool($with_grant_option) + if $with_grant_option { + $grant_option = "-WITH_GRANT_OPTION" + } + sqlserver_tsql{ "login-permission-${instance}-${login}-${_state}${grant_option}": + instance => $instance, + command => template('sqlserver/create/login/permission.sql.erb'), + onlyif => template('sqlserver/query/login/permission_exists.sql.erb'), + require => Sqlserver::Config[$instance], + } +} diff --git a/manifests/role.pp b/manifests/role.pp new file mode 100644 index 00000000..4ed7967a --- /dev/null +++ b/manifests/role.pp @@ -0,0 +1,117 @@ +## +# == Define Resource Type: sqlserver::role::permissions +# +# +# === Requirement/Dependencies: +# +# Requires defined type {sqlserver::config} in order to execute against the SQL Server instance +# +# +# === Parameters +# +# [ensure] +# Whether the role should be absent or present +# +# [role] +# The name of the role for which the permissions will be manage. +# +# [instance] +# The name of the instance where the role and database exists. Defaults to 'MSSQLSERVER' +# +# [authorization] +# The database principal that should own the role +# +# [type] +# Whether the Role is `SERVER` or `DATABASE` +# +# [database] +# The name of the database the role exists on when specifying `type => 'DATABASE'`. Defaults to 'master' +# +# [permissions] +# A hash of permissions that should be managed for the role. Valid keys are 'GRANT', 'GRANT_WITH_OPTION', 'DENY' or 'REVOKE'. Valid values must be an array of Strings i.e. {'GRANT' => ['CONNECT', 'CREATE ANY DATABASE'] } +# +# [members] +# An array of users/logins that should be a member of the role +# +# [members_purge] +# Whether we should purge any members not listed in the members parameter. Default: false +## +define sqlserver::role( + $ensure = present, + $role = $title, + $instance = 'MSSQLSERVER', + $authorization = undef, + $type = 'SERVER', + $database = 'master', + $permissions = { }, + $members = [], + $members_purge = false, +){ + sqlserver_validate_instance_name($instance) + sqlserver_validate_range($role, 1, 128, 'Role names must be between 1 and 128 characters') + + validate_re($type, ['^SERVER$','^DATABASE$'], "Type must be either 'SERVER' or 'DATABASE', provided '${type}'") + + sqlserver_validate_range($database, 1, 128, 'Database name must be between 1 and 128 characters') + if $type == 'SERVER' and $database != 'master' { + fail('Can not specify a database other than master when managing SERVER ROLES') + } + + $_create_delete = $ensure ? { + present => 'create', + absent => 'delete', + } + + sqlserver_tsql{ "role-${role}-${instance}": + command => template("sqlserver/${_create_delete}/role.sql.erb"), + onlyif => template('sqlserver/query/role_exists.sql.erb'), + instance => $instance, + } + + if $ensure == present { + validate_hash($permissions) + $_upermissions = sqlserver_upcase($permissions) + + Sqlserver::Role::Permissions{ + role => $role, + instance => $instance, + database => $database, + type => $type, + require => Sqlserver_tsql["role-${role}-${instance}"] + } + if has_key($_upermissions, 'GRANT') and is_array($_upermissions['GRANT']) { + sqlserver::role::permissions{ "Sqlserver::Role[${title}]-GRANT-${role}": + state => 'GRANT', + permissions => $_upermissions['GRANT'], + } + } + if has_key($_upermissions, 'DENY') and is_array($_upermissions['DENY']) { + sqlserver::role::permissions{ "Sqlserver::Role[${title}]-DENY-${role}": + state => 'DENY', + permissions => $_upermissions['DENY'], + } + } + if has_key($_upermissions, 'REVOKE') and is_array($_upermissions['REVOKE']) { + sqlserver::role::permissions{ "Sqlserver::Role[${title}]-REVOKE-${role}": + state => 'REVOKE', + permissions => $_upermissions['REVOKE'], + } + } + if has_key($_upermissions, 'GRANT_WITH_OPTION') and is_array($_upermissions['GRANT_WITH_OPTION']) { + sqlserver::role::permissions{ "Sqlserver::Role[${title}]-GRANT-WITH_GRANT_OPTION-${role}": + state => 'GRANT', + with_grant_option => true, + permissions => $_upermissions['GRANT_WITH_OPTION'], + } + } + + validate_array($members) + if size($members) > 0 or $members_purge == true { + sqlserver_tsql{ "role-${role}-members": + command => template('sqlserver/create/role/members.sql.erb'), + onlyif => template('sqlserver/query/role/member_exists.sql.erb'), + instance => $instance, + } + } + } +} diff --git a/manifests/role/permissions.pp b/manifests/role/permissions.pp new file mode 100644 index 00000000..cb80072c --- /dev/null +++ b/manifests/role/permissions.pp @@ -0,0 +1,74 @@ +## +# == Define Resource Type: sqlserver::role::permissions +# +# +# === Requirement/Dependencies: +# +# Requires defined type {sqlserver::config} in order to execute against the SQL Server instance +# +# +# === Parameters +# [role] +# The name of the role for which the permissions will be manage. +# +# [permissions] +# An array of permissions you want manged for the given role +# +# [state] +# The state you would like the permission in. Accepts 'GRANT', 'DENY', 'REVOKE' Please note that REVOKE equates to absent and will default to database and system level permissions. +# +# [with_grant_option] +# Whether to give the role the option to grant this permission to other principal objects, accepts true or false, defaults to false +# +# [type] +# Whether the Role is `SERVER` or `DATABASE` +# +# [database] +# The name of the database the role exists on when specifying `type => 'DATABASE'`. Defaults to 'master' +# +# [instance] +# The name of the instance where the role and database exists. Defaults to 'MSSQLSERVER' +# +## +define sqlserver::role::permissions ( + $role, + $permissions, + $state = 'GRANT', + $with_grant_option = false, + $type = 'SERVER', + $database = 'master', + $instance = 'MSSQLSERVER', +){ + validate_array($permissions) + if size($permissions) < 1 { + warning("Received an empty set of permissions for ${title}, no further action will be taken") + } else{ + sqlserver_validate_instance_name($instance) + #Validate state + $_state = upcase($state) + validate_re($_state,'^(GRANT|REVOKE|DENY)$',"State can only be of 'GRANT', 'REVOKE' or 'DENY' you passed ${state}") + validate_bool($with_grant_option) + + #Validate role + sqlserver_validate_range($role, 1, 128, 'Role names must be between 1 and 128 characters') + + #Validate permissions + sqlserver_validate_range($permissions, 4, 128, 'Permissions must be between 4 and 128 characters') + + $_upermissions = upcase($permissions) + + $_grant_option = $with_grant_option ? { + true => '-WITH_GRANT_OPTION', + false => '', + } + ## + # Parameters required in template are _state, role, _upermissions, database, type, with_grant_option + ## + sqlserver_tsql{ "role-permissions-${role}-${_state}${_grant_option}-${instance}": + instance => $instance, + command => template('sqlserver/create/role/permissions.sql.erb'), + onlyif => template('sqlserver/query/role/permission_exists.sql.erb'), + } + } + +} diff --git a/manifests/user.pp b/manifests/user.pp new file mode 100644 index 00000000..29139e83 --- /dev/null +++ b/manifests/user.pp @@ -0,0 +1,113 @@ +## +# == Define Resource Type: sqlserver::user +# +# === Requirement/Dependencies: +# +# Requires defined type {sqlserver::config} in order to execute against the SQL Server instance +# +# === Examples +# +# sqlserver::user{'myUser': +# database => 'loggingDatabase', +# login => 'myUser', +# } +# +# === Parameters +# [user] +# The username you want to manage, defaults to the title +# +# [database] +# The database you want the user to be created as +# +# [ensure] +# Ensure present or absent +# +# [default_schema] +# SQL schema you would like to default to, typically 'dbo' +# +# [instance] +# The named instance you want to manage against +# +# [login] +# The login to associate the user with, by default SQL Server will assume user and login match if left empty +# +# [password] +# The password for the user, can only be used when the database is a contained database. +# +# [permissions] +# A hash of permissions that should be managed for the user. Valid keys are 'GRANT', 'GRANT_WITH_OPTION', 'DENY' or 'REVOKE'. Valid values must be an array of Strings i.e. {'GRANT' => ['SELECT', 'INSERT'] } +# +## +define sqlserver::user ( + $database, + $ensure = 'present', + $user = $title, + $default_schema = undef, + $instance = 'MSSQLSERVER', + $login = undef, + $password = undef, + $permissions = { }, +) +{ + sqlserver_validate_instance_name($instance) + + $is_windows_user = sqlserver_is_domain_or_local_user($login) + + if $password { + sqlserver_validate_range($password, 1, 128, 'Password must be equal or less than 128 characters') + if $is_windows_user and $login != undef{ + fail('Can not provide password when using a Windows Login') + } + } + sqlserver_validate_range($database, 1, 128, 'Database name must be between 1 and 128 characters') + + $create_delete = $ensure ? { + present => 'create', + absent => 'delete', + } + + sqlserver_tsql{ "user-${instance}-${database}-${user}": + instance => $instance, + command => template("sqlserver/${create_delete}/user.sql.erb"), + onlyif => template('sqlserver/query/user_exists.sql.erb'), + require => Sqlserver::Config[$instance] + } + + if $ensure == present { + validate_hash($permissions) + $_upermissions = sqlserver_upcase($permissions) + sqlserver_validate_hash_uniq_values($_upermissions, "Duplicate permissions found for sqlserver::user[${title}]") + + Sqlserver::User::Permissions{ + user => $user, + database => $database, + instance => $instance, + require => Sqlserver_tsql["user-${instance}-${database}-${user}"] + } + if has_key($_upermissions, 'GRANT') and is_array($_upermissions['GRANT']) { + sqlserver::user::permissions{ "Sqlserver::User[${title}]-GRANT-${user}": + state => 'GRANT', + permissions => $_upermissions['GRANT'], + } + } + if has_key($_upermissions, 'DENY') and is_array($_upermissions['DENY']) { + sqlserver::user::permissions{ "Sqlserver::User[${title}]-DENY-${user}": + state => 'DENY', + permissions => $_upermissions['DENY'], + } + } + if has_key($_upermissions, 'REVOKE') and is_array($_upermissions['REVOKE']) { + sqlserver::user::permissions{ "Sqlserver::User[${title}]-REVOKE-${user}": + state => 'REVOKE', + permissions => $_upermissions['REVOKE'], + } + } + if has_key($_upermissions, 'GRANT_WITH_OPTION') and is_array($_upermissions['GRANT_WITH_OPTION']) { + sqlserver::user::permissions{ "Sqlserver::User[${title}]-GRANT-WITH_GRANT_OPTION-${user}": + state => 'GRANT', + with_grant_option => true, + permissions => $_upermissions['GRANT_WITH_OPTION'], + } + } + } +} diff --git a/manifests/user/permissions.pp b/manifests/user/permissions.pp new file mode 100644 index 00000000..06cc902e --- /dev/null +++ b/manifests/user/permissions.pp @@ -0,0 +1,66 @@ +## +# == Define Resource Type: sqlserver::user::permissions +# +# === Requirement/Dependencies: +# +# Requires defined type {sqlserver::config} in order to execute against the SQL Server instance +# +# +# === Parameters +# [user] +# The username for which the permission will be manage. +# +# [database] +# The databaser you would like the permission managed on. +# +# [permissions] +# An array of permissions you would like managed. i.e. ['SELECT', 'INSERT', 'UPDATE', 'DELETE'] +# +# [state] +# The state you would like the permission in. Accepts 'GRANT', 'DENY', 'REVOKE' Please note that REVOKE equates to absent and will default to database and system level permissions. +# +# [with_grant_option] +# Whether to give the user the option to grant this permission to other users, accepts true or false, defaults to false +# +# [instance] +# The name of the instance where the user and database exists. Defaults to 'MSSQLSERVER' +# +## +define sqlserver::user::permissions ( + $user, + $database, + $permissions, + $state = 'GRANT', + $with_grant_option = false, + $instance = 'MSSQLSERVER', +){ + sqlserver_validate_instance_name($instance) + +## Validate Permissions + sqlserver_validate_range($permissions, 4, 128, 'Permission must be between 4 and 128 characters') + validate_array($permissions) + +## Validate state + $_state = upcase($state) + validate_re($_state,'^(GRANT|REVOKE|DENY)$',"State can only be of 'GRANT', 'REVOKE' or 'DENY' you passed ${state}") + + validate_bool($with_grant_option) + if $with_grant_option and $_state != 'GRANT' { + fail("Can not use with_grant_option and state ${_state}, must be 'GRANT'") + } + + sqlserver_validate_range($database, 1, 128, 'Database must be between 1 and 128 characters') + + sqlserver_validate_range($user, 1, 128, 'User must be between 1 and 128 characters') + + if $with_grant_option { + $grant_option = "-WITH_GRANT_OPTION" + } + sqlserver_tsql{ + "user-permissions-${instance}-${database}-${user}-${_state}${grant_option}": + instance => $instance, + command => template("sqlserver/create/user/permission.sql.erb"), + onlyif => template('sqlserver/query/user/permission_exists.sql.erb'), + require => Sqlserver::Config[$instance], + } +} diff --git a/spec/defines/config_spec.rb b/spec/defines/config_spec.rb new file mode 100644 index 00000000..3c2c7017 --- /dev/null +++ b/spec/defines/config_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' +require File.expand_path(File.join(File.dirname(__FILE__), 'manifest_shared_examples.rb')) + +RSpec.describe 'sqlserver::config', :type => :define do + let(:title) { 'MSSQLSERVER' } + let(:params) { { + :instance_name => 'MSSQLSERVER', + :admin_user => 'sa', + :admin_pass => 'Pupp3t1@', + } } + let(:facts) { {:osfamily => 'windows', :platform => :windows} } + describe 'compile' do + it { + should contain_file('C:/Program Files/Microsoft SQL Server/.puppet/.MSSQLSERVER.cfg') + should contain_file('C:/Program Files/Microsoft SQL Server/.puppet') + } + end +end diff --git a/spec/defines/database_spec.rb b/spec/defines/database_spec.rb index e4f26342..c30edf68 100644 --- a/spec/defines/database_spec.rb +++ b/spec/defines/database_spec.rb @@ -12,7 +12,12 @@ end describe 'Minimal Params' do - it_behaves_like 'sqlserver_tsql command' + let(:pre_condition) { <<-EOF + define sqlserver::config{} + sqlserver::config {'MSSQLSERVER': } + EOF + } + it_behaves_like 'compile' end describe 'Providing log filespec it should compile with valid log on params and' do diff --git a/spec/defines/login/permissions_spec.rb b/spec/defines/login/permissions_spec.rb new file mode 100644 index 00000000..70124f63 --- /dev/null +++ b/spec/defines/login/permissions_spec.rb @@ -0,0 +1,135 @@ +require 'spec_helper' +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'manifest_shared_examples.rb')) + +describe 'sqlserver::login::permissions' do + let(:facts) { {:osfamily => 'windows'} } + context 'validation errors' do + include_context 'manifests' do + let(:title) { 'myTitle' } + let(:sqlserver_tsql_title) { 'login-permission-MSSQLSERVER-loggingUser-GRANT' } + end + context 'login =>' do + let(:params) { { + :permissions => ['SELECT'], + } } + let(:raise_error_check) { 'Login must be between 1 and 128 characters' } + describe 'missing' do + let(:raise_error_check) { 'Must pass login to Sqlserver::Login::Permissions[myTitle]' } + it_behaves_like 'validation error' + end + describe 'empty' do + let(:additional_params) { {:login => ''} } + it_behaves_like 'validation error' + end + describe 'over limit' do + let(:additional_params) { {:login => random_string_of_size(129)} } + it_behaves_like 'validation error' + end + end + context 'permission' do + let(:params) { { + :login => 'loggingUser', + } } + let(:raise_error_check) { 'Permission must be between 4 and 128 characters' } + describe 'empty' do + let(:additional_params) { {:permissions => ['']} } + it_behaves_like 'validation error' + end + describe 'under limit' do + let(:additional_params) { {:permissions => [random_string_of_size(3, false)]} } + it_behaves_like 'validation error' + end + describe 'over limit' do + let(:additional_params) { {:permissions => [random_string_of_size(129, false)]} } + it_behaves_like 'validation error' + end + end + context 'state =>' do + let(:params) { { + :permissions => ['SELECT'], + :login => 'loggingUser' + } } + describe 'invalid' do + let(:additional_params) { {:state => 'invalid'} } + let(:raise_error_check) { "State parameter can only be one of 'GRANT', 'REVOKE' or 'DENY', you passed a value of invalid" } + it_behaves_like 'validation error' + end + end + end + context 'successfully' do + include_context 'manifests' do + let(:title) { 'myTitle' } + let(:sqlserver_tsql_title) { 'login-permission-MSSQLSERVER-loggingUser-GRANT' } + let(:params) { { + :login => 'loggingUser', + :permissions => ['SELECT'], + } } + end + %w(revoke grant deny).each do |state| + context "state => '#{state}'" do + let(:sqlserver_tsql_title) { "login-permission-MSSQLSERVER-loggingUser-#{state.upcase}" } + let(:should_contain_command) { ["#{state.upcase} SELECT TO [loggingUser];", 'USE [master];'] } + describe "lowercase #{state}" do + let(:additional_params) { {:state => state} } + it_behaves_like 'sqlserver_tsql command' + end + state.capitalize! + describe "capitalized #{state}" do + let(:additional_params) { {:state => state} } + it_behaves_like 'sqlserver_tsql command' + end + end + end + + context 'permission' do + describe 'upper limit' do + permission =random_string_of_size(128, false) + let(:additional_params) { {:permissions => [permission]} } + let(:sqlserver_tsql_title) { "login-permission-MSSQLSERVER-loggingUser-GRANT" } + let(:should_contain_command) { ['USE [master];'] } + it_behaves_like 'sqlserver_tsql command' + end + describe 'alter' do + let(:additional_params) { {:permissions => ['ALTER']} } + let(:should_contain_command) { ['USE [master];', 'GRANT ALTER TO [loggingUser];'] } + let(:sqlserver_tsql_title) { "login-permission-MSSQLSERVER-loggingUser-GRANT" } + it_behaves_like 'sqlserver_tsql command' + end + end + + describe 'Minimal Params' do + let(:pre_condition) { <<-EOF + define sqlserver::config{} + sqlserver::config {'MSSQLSERVER': } + EOF + } + it_behaves_like 'compile' + end + + end + + context 'command syntax' do + include_context 'manifests' do + let(:title) { 'myTitle' } + let(:sqlserver_tsql_title) { 'login-permission-MSSQLSERVER-loggingUser-GRANT' } + let(:params) { { + :login => 'loggingUser', + :permissions => ['SELECT'], + } } + describe '' do + let(:should_contain_command) { [ + 'USE [master];', + 'GRANT SELECT TO [loggingUser];', + /DECLARE @perm_state varchar\(250\)/, + /SET @perm_state = ISNULL\(\n\s+\(SELECT perm.state_desc FROM sys\.server_permissions perm\n\s+JOIN sys\./, + /JOIN sys\.server_principals princ ON princ.principal_id = perm\.grantee_principal_id\n\s+WHERE/, + /WHERE princ\.type IN \('U','S','G'\)\n\s+ AND princ\.name = 'loggingUser'\n\s+AND perm\.permission_name = @permission\),\n\s+'REVOKE'\)/, + /SET @error_msg = 'EXPECTED login \[loggingUser\] to have permission \[' \+ @permission \+ '\] with GRANT but got ' \+ @perm_state;/, + /IF @perm_state != 'GRANT'\n\s+THROW 51000, @error_msg, 10/ + ] } + it_behaves_like 'sqlserver_tsql command' + end + end + end + +end diff --git a/spec/defines/login_spec.rb b/spec/defines/login_spec.rb index c126e01d..72faad20 100644 --- a/spec/defines/login_spec.rb +++ b/spec/defines/login_spec.rb @@ -6,8 +6,8 @@ let(:sqlserver_tsql_title) { 'login-MSSQLSERVER-myTitle' } let(:title) { 'myTitle' } let(:params) { { - :login => 'myTitle', - :instance => 'MSSQLSERVER', + :login => 'myTitle', + :instance => 'MSSQLSERVER', } } end @@ -18,19 +18,19 @@ end describe 'parameter assignment' do let(:should_contain_command) { [ - "IF exists(select * from sys.sql_logins where name = 'myTitle')", - "@login as varchar(255) = 'myTitle'", - '@is_disabled as tinyint = 0' + "IF exists(select * from sys.sql_logins where name = 'myTitle')", + "@login as varchar(255) = 'myTitle'", + '@is_disabled as tinyint = 0' ] } let(:should_contain_onlyif) { [ - "@login as varchar(255) = 'myTitle'", - "@is_disabled as tinyint = 0", - "@check_expiration as tinyint = 0", - "@check_policy as tinyint = 1", - "@type_desc as varchar(50) = 'SQL_LOGIN'", - "@default_db as varchar(255) = 'master'", - "@default_lang as varchar(50) = 'us_english'", - "IF NOT EXISTS(SELECT name FROM sys.server_principals WHERE name = 'myTitle')" + "@login as varchar(255) = 'myTitle'", + "@is_disabled as tinyint = 0", + "@check_expiration as tinyint = 0", + "@check_policy as tinyint = 1", + "@type_desc as varchar(50) = 'SQL_LOGIN'", + "@default_db as varchar(255) = 'master'", + "@default_lang as varchar(50) = 'us_english'", + "IF NOT EXISTS(SELECT name FROM sys.server_principals WHERE name = 'myTitle')" ] } it_behaves_like 'sqlserver_tsql command' it_behaves_like 'sqlserver_tsql onlyif' @@ -40,4 +40,77 @@ let(:raise_error_check) { 'Can not have check expiration enabled when check_policy is disabled' } it_should_behave_like 'validation error' end + context 'permissions =>' do + let(:title) { 'myTitle' } + let(:params) { { + :login => 'myLogin', + } } + let(:permissions) { {} } + shared_examples 'sqlserver_permissions exists' do |type| + it { + params[:permissions] = permissions + type_title = (type =~ /GRANT_WITH_OPTION/i ? 'GRANT-WITH_GRANT_OPTION' : type.upcase) + should contain_sqlserver__login__permissions("Sqlserver::Login[#{title}]-#{type_title}-myLogin").with( + { + 'login' => 'myLogin', + 'state' => type == 'GRANT_WITH_OPTION' ? 'GRANT' : type.upcase, + 'with_grant_option' => type == 'GRANT_WITH_OPTION', + 'permissions' => permissions[type], + 'require' => 'Sqlserver_tsql[login-MSSQLSERVER-myLogin]' + } + ) + } + end + + shared_examples 'sqlserver_permissions absent' do |type| + it { + params[:permissions] = permissions + type_title = (type =~ /GRANT_WITH_OPTION/i ? 'GRANT-WITH_GRANT_OPTION' : type.upcase) + should_not contain_sqlserver__login__permissions("Sqlserver::Login[#{title}]-#{type_title}-myLogin") + } + end + + describe 'GRANT permissions' do + let(:permissions) { {'GRANT' => ['SELECT']} } + it_behaves_like 'sqlserver_permissions exists', 'GRANT' + it_behaves_like 'sqlserver_permissions absent', 'DENY' + it_behaves_like 'sqlserver_permissions absent', 'REVOKE' + it_behaves_like 'sqlserver_permissions absent', 'GRANT_WITH_OPTION' + end + + describe 'GRANT DENY' do + let(:permissions) { {'GRANT' => ['CONNECT SQL'], 'DENY' => ['INSERT']} } + it_behaves_like 'sqlserver_permissions exists', 'GRANT' + it_behaves_like 'sqlserver_permissions exists', 'DENY' + it_behaves_like 'sqlserver_permissions absent', 'REVOKE' + it_behaves_like 'sqlserver_permissions absent', 'GRANT_WITH_OPTION' + end + + describe 'GRANT_WITH_OPTION' do + let(:permissions) { {'GRANT_WITH_OPTION' => ['CONNECT SQL']} } + it_behaves_like 'sqlserver_permissions exists', 'GRANT_WITH_OPTION' + end + + describe 'REVOKE' do + let(:permissions) { {'revoke' => ['CREATE ANY DATABASE']} } + it_behaves_like 'sqlserver_permissions exists', 'revoke' + it_behaves_like 'sqlserver_permissions absent', 'GRANT' + it_behaves_like 'sqlserver_permissions absent', 'DENY' + it_behaves_like 'sqlserver_permissions absent', 'GRANT_WITH_OPTION' + end + + describe 'empty' do + %w(GRANT DENY REVOKE GRANT-WITH_GRANT_OPTION).each do |type| + it_behaves_like 'sqlserver_permissions absent', type + end + end + + describe 'duplicate permissions' do + let(:additional_params) { { + :permissions => {'GRANT' => ['CONNECT SQL'], 'REVOKE' => ['CONNECT SQL']} + } } + let(:raise_error_check) { "Duplicate permissions found for sqlserver::login[#{title}" } + it_behaves_like 'validation error' + end + end end diff --git a/spec/defines/manifest_shared_examples.rb b/spec/defines/manifest_shared_examples.rb index 33762698..4094b12e 100644 --- a/spec/defines/manifest_shared_examples.rb +++ b/spec/defines/manifest_shared_examples.rb @@ -42,6 +42,7 @@ def convert_to_regexp(str) } end + shared_examples 'sqlserver_tsql without_command' do it { params.merge!(additional_params) @@ -50,12 +51,14 @@ def convert_to_regexp(str) end } end + shared_examples 'compile' do it { params.merge!(additional_params) should compile } end + shared_examples 'validation error' do it { params.merge!(additional_params) @@ -63,3 +66,10 @@ def convert_to_regexp(str) } end end + +def random_string_of_size(size, include_numeric = true) + pool = [('a'..'z'), ('A'..'Z')] + pool << (0..9) if include_numeric + o = pool.map { |i| i.to_a }.flatten + (0...size).map { o[rand(o.length)] }.join +end diff --git a/spec/defines/role/permissions_spec.rb b/spec/defines/role/permissions_spec.rb new file mode 100644 index 00000000..1a615478 --- /dev/null +++ b/spec/defines/role/permissions_spec.rb @@ -0,0 +1,143 @@ +require 'spec_helper' + +RSpec.describe 'sqlserver::role::permissions' do + include_context 'manifests' do + let(:title) { 'myTitle' } + let(:sqlserver_tsql_title) { 'role-permissions-myCustomRole-GRANT-MSSQLSERVER' } + let(:params) { { + :role => 'myCustomRole', + :permissions => %w(INSERT UPDATE DELETE SELECT), + } } + end + + context 'sql variables' do + let(:params) { { + :role => 'myCustomRole', + :permissions => %w(INSERT UPDATE DELETE SELECT), + } } + declare_variables = [ + "DECLARE + @perm_state varchar(250), + @error_msg varchar(250), + @permission varchar(250), + @princ_name varchar(50), + @princ_type varchar(50), + @state_desc varchar(50);", + "SET @princ_type = 'SERVER_ROLE';", + "SET @princ_name = 'myCustomRole';", + "SET @state_desc = 'GRANT';"] + let(:should_contain_command) { declare_variables } + let(:should_contain_onlyif) { declare_variables } + it_behaves_like 'sqlserver_tsql command' + it_behaves_like 'sqlserver_tsql onlyif' + end + + context 'type =>' do + shared_examples 'GRANT Permissions' do |type| + base_commands = [ + "SET @princ_type = '#{type.upcase}_ROLE';", + "ISNULL( + (SELECT state_desc FROM sys.#{type.downcase}_permissions prem + JOIN sys.#{type.downcase}_principals r ON r.principal_id = prem.grantee_principal_id + WHERE r.name = @princ_name AND r.type_desc = @princ_type + AND prem.permission_name = @permission), + 'REVOKE')", + "SET @permission = 'INSERT';", + "SET @permission = 'UPDATE';", + "SET @permission = 'DELETE';", + "SET @permission = 'SELECT';", + ] + should_commands = [ + "GRANT INSERT TO [myCustomRole];", + "GRANT UPDATE TO [myCustomRole];", + "GRANT DELETE TO [myCustomRole];", + "GRANT SELECT TO [myCustomRole];" + ] + let(:should_contain_command) { base_commands + should_commands } + let(:should_contain_onlyif) { base_commands } + it_behaves_like 'sqlserver_tsql command' + it_behaves_like 'sqlserver_tsql onlyif' + end + + describe 'DATABASE' do + let(:additional_params) { { + :type => 'DATABASE', + } } + it_behaves_like 'GRANT Permissions', 'database' + end + + describe 'SERVER' do + let(:additional_params) { { + :type => 'SERVER', + } } + it_behaves_like 'GRANT Permissions', 'server' + end + end + + context 'permissions =>' do + describe '[INSERT UPDATE DELETE SELECT]' do + declare_variables = [ + "SET @permission = 'INSERT';", + "SET @permission = 'UPDATE';", + "SET @permission = 'DELETE';", + "SET @permission = 'SELECT';", + ] + let(:should_contain_command) { declare_variables + + [ + "GRANT INSERT TO [myCustomRole];", + "GRANT UPDATE TO [myCustomRole];", + "GRANT DELETE TO [myCustomRole];", + "GRANT SELECT TO [myCustomRole];" + ] } + let(:should_contain_onlyif) { declare_variables } + it_behaves_like 'sqlserver_tsql command' + it_behaves_like 'sqlserver_tsql onlyif' + end + describe '[]' do + let(:params) { { + :role => 'myCustomRole', + :permissions => [] + } } + it { + should compile + should_not contain_sqlserver_tsql(sqlserver_tsql_title) + } + end + end + + context 'database =>' do + describe 'default' do + let(:should_contain_command) { ['USE [master];'] } + let(:should_contain_onlyif) { ['USE [master];'] } + it_behaves_like 'sqlserver_tsql command' + it_behaves_like 'sqlserver_tsql onlyif' + end + describe 'customDatabase' do + let(:additional_params) { {:database => 'customDatabase'} } + let(:should_contain_command) { ['USE [customDatabase];'] } + it_behaves_like 'sqlserver_tsql command' + let(:should_contain_onlyif) { ['USE [customDatabase];'] } + it_behaves_like 'sqlserver_tsql onlyif' + let(:should_contain_without_command) { ['USE [master];'] } + it_behaves_like 'sqlserver_tsql without_command' + let(:should_contain_without_onlyif) { ['USE [master];'] } + it_behaves_like 'sqlserver_tsql without_onlyif' + end + end + + context 'instance =>' do + ['MSSQLSERVER', 'MYINSTANCE'].each do |instance| + describe "should contain #{instance} for sqlserver_tsql" do + let(:params) { { + :role => 'myCustomRole', + :permissions => %w(INSERT UPDATE DELETE SELECT), + :instance => instance + } } + it { + should contain_sqlserver_tsql("role-permissions-myCustomRole-GRANT-#{instance}").with_instance(instance) + } + end + end + end + +end diff --git a/spec/defines/role_spec.rb b/spec/defines/role_spec.rb new file mode 100644 index 00000000..cba659d6 --- /dev/null +++ b/spec/defines/role_spec.rb @@ -0,0 +1,236 @@ +require 'spec_helper' +require File.expand_path(File.join(File.dirname(__FILE__), 'manifest_shared_examples.rb')) + +RSpec.describe 'sqlserver::role', :type => :define do + include_context 'manifests' do + let(:sqlserver_tsql_title) { 'role-myCustomRole-MSSQLSERVER' } + let(:title) { 'myCustomRole' } + end + + context 'type =>' do + describe 'invalid' do + let(:additional_params) { { + :type => 'invalid', + } } + let(:raise_error_check) { "Type must be either 'SERVER' or 'DATABASE', provided 'invalid'" } + it_behaves_like 'validation error' + end + describe 'SERVER' do + let(:should_contain_command) { [ + 'USE [master];', + 'CREATE SERVER ROLE [myCustomRole];', + /IF NOT EXISTS\(\n\s+SELECT name FROM sys\.server_principals WHERE type_desc = 'SERVER_ROLE' AND name = 'myCustomRole'\n\)/, + "THROW 51000, 'The SERVER ROLE [myCustomRole] does not exist', 10" + ] } + let(:should_contain_onlyif) { [ + /IF NOT EXISTS\(\n\s+SELECT name FROM sys\.server_principals WHERE type_desc = 'SERVER_ROLE' AND name = 'myCustomRole'\n\)/, + "THROW 51000, 'The SERVER ROLE [myCustomRole] does not exist', 10" + ] } + it_behaves_like 'sqlserver_tsql command' + it_behaves_like 'sqlserver_tsql onlyif' + end + describe 'DATABASE' do + let(:additional_params) { { + 'type' => 'DATABASE', + } } + let(:should_contain_command) { [ + 'USE [master];', + 'CREATE ROLE [myCustomRole];', + /IF NOT EXISTS\(\n\s+SELECT name FROM sys\.database_principals WHERE type_desc = 'DATABASE_ROLE' AND name = 'myCustomRole'\n\)/, + "THROW 51000, 'The DATABASE ROLE [myCustomRole] does not exist', 10" + ] } + let(:should_contain_onlyif) { [ + /IF NOT EXISTS\(\n\s+SELECT name FROM sys\.database_principals WHERE type_desc = 'DATABASE_ROLE' AND name = 'myCustomRole'\n\)/, + "THROW 51000, 'The DATABASE ROLE [myCustomRole] does not exist', 10", + ] } + + it_behaves_like 'sqlserver_tsql command' + it_behaves_like 'sqlserver_tsql onlyif' + + end + end + + context 'database =>' do + let(:additional_params) { { + 'database' => 'myCrazyDb', + } } + describe 'with server role type' do + let(:raise_error_check) { 'Can not specify a database other than master when managing SERVER ROLES' } + it_behaves_like 'validation error' + end + describe 'with database role type' do + let(:additional_params) { { + 'database' => 'myCrazyDb', + 'type' => 'DATABASE', + } } + let(:should_contain_command) { [ + 'USE [myCrazyDb];', + ] } + it_behaves_like 'sqlserver_tsql command' + end + end + + context 'instance =>' do + describe 'non default instance' do + let(:params) { {:instance => 'MYCUSTOM'} } + it { + should contain_sqlserver_tsql('role-myCustomRole-MYCUSTOM').with_instance('MYCUSTOM') + } + end + describe 'empty instance' do + let(:additional_params) { {'instance' => ''} } + let(:raise_error_check) { 'Instance name must be between 1 to 16 characters' } + it_behaves_like 'validation error' + end + end + + context 'authorization =>' do + describe 'undef' do + let(:should_not_contain_command) { [ + /AUTHORIZATION/i, + 'ALTER AUTHORIZATION ON ', + ] } + it_behaves_like 'sqlserver_tsql without_command' + end + describe 'myUser' do + let(:additional_params) { { + :authorization => 'myUser', + } } + let(:should_contain_command) { [ + 'CREATE SERVER ROLE [myCustomRole] AUTHORIZATION [myUser];', + 'ALTER AUTHORIZATION ON SERVER ROLE::[myCustomRole] TO [myUser];' + ] } + it_behaves_like 'sqlserver_tsql command' + end + describe 'myUser on Database' do + let(:additional_params) { { + :authorization => 'myUser', + :type => 'DATABASE', + } } + let(:should_contain_command) { [ + 'CREATE ROLE [myCustomRole] AUTHORIZATION [myUser];', + 'ALTER AUTHORIZATION ON ROLE::[myCustomRole] TO [myUser];' + ] } + it_behaves_like 'sqlserver_tsql command' + end + end + + context 'ensure =>' do + describe 'absent' do + let(:additional_params) { { + :ensure => 'absent', + } } + let(:should_contain_command) { [ + 'USE [master];', + 'DROP SERVER ROLE [myCustomRole];' + ] } + let(:should_contain_onlyif) { [ + 'IF EXISTS(', + ] } + it_behaves_like 'sqlserver_tsql command' + it_behaves_like 'sqlserver_tsql onlyif' + end + end + + context 'members =>' do + let(:sqlserver_tsql_title) { 'role-myCustomRole-members' } + describe '[test these users]' do + let(:additional_params) { { + :members => %w(test these users), + } } + let(:should_contain_command) { [ + 'ALTER SERVER ROLE [myCustomRole] ADD MEMBER [test];', + 'ALTER SERVER ROLE [myCustomRole] ADD MEMBER [these];', + 'ALTER SERVER ROLE [myCustomRole] ADD MEMBER [users];', + ] } + let(:should_contain_onlyif) { [ + ] } + it_behaves_like 'sqlserver_tsql command' + it_behaves_like 'sqlserver_tsql onlyif' + end + describe 'empty' do + it { + should_not contain_sqlserver_tsql(sqlserver_tsql_title) + } + end + end + context 'members_purge =>' do + let(:sqlserver_tsql_title) { 'role-myCustomRole-members' } + context 'true' do + describe 'type => SERVER and members => []' do + let(:additional_params) { { + :members_purge => true, + } } + let(:should_contain_command) { [ + "WHILE(@row <= @row_count) +BEGIN + SET @sql = 'ALTER SERVER ROLE [myCustomRole] DROP MEMBER [' + (SELECT member FROM @purge_members WHERE ID = @row) + '];' + EXEC(@sql) + SET @row += 1 +END" + ] } + let(:should_contain_onlyif) { [ + "DECLARE @purge_members TABLE ( +ID int IDENTITY(1,1), +member varchar(128) +)", + "INSERT INTO @purge_members (member) ( +SELECT m.name FROM sys.server_role_members rm + JOIN sys.server_principals r ON rm.role_principal_id = r.principal_id + JOIN sys.server_principals m ON rm.member_principal_id = m.principal_id + WHERE r.name = 'myCustomRole'", + "IF 0 != (SELECT COUNT(*) FROM @purge_members) + THROW 51000, 'Unlisted Members in Role, will be purged', 10", + ] } + it_behaves_like 'sqlserver_tsql command' + it_behaves_like 'sqlserver_tsql onlyif' + end + + describe 'type => DATABASE and members => []' do + let(:additional_params) { { + :type => 'DATABASE', + :members_purge => true, + } } + let(:should_contain_command) { [ + "WHILE(@row <= @row_count) +BEGIN + SET @sql = 'ALTER ROLE [myCustomRole] DROP MEMBER [' + (SELECT member FROM @purge_members WHERE ID = @row) + '];' + EXEC(@sql) + SET @row += 1 +END" + ] } + let(:should_contain_onlyif) { [ + "DECLARE @purge_members TABLE ( +ID int IDENTITY(1,1), +member varchar(128) +)", + "INSERT INTO @purge_members (member) ( +SELECT m.name FROM sys.database_role_members rm + JOIN sys.database_principals r ON rm.role_principal_id = r.principal_id + JOIN sys.database_principals m ON rm.member_principal_id = m.principal_id + WHERE r.name = 'myCustomRole'", + "IF 0 != (SELECT COUNT(*) FROM @purge_members) + THROW 51000, 'Unlisted Members in Role, will be purged', 10", + ] } + it_behaves_like 'sqlserver_tsql command' + it_behaves_like 'sqlserver_tsql onlyif' + end + end + describe '[test these users]' do + let(:additional_params) { { + :members_purge => true, + :members => %w(test these users), + } } + let(:should_contain_command) { [ + /WHERE r\.name = 'myCustomRole'\n\s+AND m\.name NOT IN \(/, + "NOT IN ('test','these','users')" + ] } + let(:should_contain_onlyif) { [ + /WHERE r\.name = 'myCustomRole'\n\s+AND m\.name NOT IN \(/, + "NOT IN ('test','these','users')" + ] } + it_behaves_like 'sqlserver_tsql command' + it_behaves_like 'sqlserver_tsql onlyif' + end + end +end diff --git a/spec/defines/user/permissions_spec.rb b/spec/defines/user/permissions_spec.rb new file mode 100644 index 00000000..dfbf06f0 --- /dev/null +++ b/spec/defines/user/permissions_spec.rb @@ -0,0 +1,187 @@ +require 'spec_helper' +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'manifest_shared_examples.rb')) + +describe 'sqlserver::user::permissions' do + let(:facts) { {:osfamily => 'windows'} } + context 'validation errors' do + include_context 'manifests' do + let(:title) { 'myTitle' } + let(:sqlserver_tsql_title) { 'user-permissions-MSSQLSERVER-loggingDb-loggingUser-GRANT' } + end + context 'user =>' do + let(:params) { { + :permissions => ['SELECT'], + :database => 'loggingDb', + } } + let(:raise_error_check) { 'User must be between 1 and 128 characters' } + describe 'missing' do + let(:raise_error_check) { 'Must pass user to Sqlserver::User::Permissions[myTitle]' } + it_behaves_like 'validation error' + end + describe 'empty' do + let(:additional_params) { {:user => ''} } + it_behaves_like 'validation error' + end + describe 'over limit' do + let(:additional_params) { {:user => random_string_of_size(129)} } + it_behaves_like 'validation error' + end + end + context 'permissions' do + let(:params) { { + :user => 'loggingUser', + :database => 'loggingDb', + } } + let(:raise_error_check) { 'Permission must be between 4 and 128 characters' } + describe 'empty' do + let(:additional_params) { {:permissions => ''} } + it_behaves_like 'validation error' + end + describe 'under limit' do + let(:additional_params) { {:permissions => [random_string_of_size(3, false)]} } + it_behaves_like 'validation error' + end + describe 'over limit' do + let(:additional_params) { {:permissions => [random_string_of_size(129, false)]} } + it_behaves_like 'validation error' + end + end + context 'state =>' do + let(:params) { { + :permissions => ['SELECT'], + :database => 'loggingDb', + :user => 'loggingUser' + } } + describe 'invalid' do + let(:additional_params) { {:state => 'invalide'} } + let(:raise_error_check) { "State can only be of 'GRANT', 'REVOKE' or 'DENY' you passed invalide" } + it_behaves_like 'validation error' + end + end + context 'with_grant_option => ' do + let(:params) { { + :permissions => ['SELECT'], + :database => 'loggingDb', + :user => 'loggingUser', + + } } + describe 'true AND state => DENY' do + let(:additional_params) { {:with_grant_option => true, :state => 'DENY'} } + let(:raise_error_check) { "Can not use with_grant_option and state DENY, must be 'GRANT' " } + it_behaves_like 'validation error' + end + describe 'invalid' do + let(:additional_params) { {:with_grant_option => 'invalid'} } + let(:raise_error_check) { '"invalid" is not a boolean' } + it_behaves_like 'validation error' + end + end + end + context 'successfully' do + include_context 'manifests' do + let(:title) { 'myTitle' } + let(:sqlserver_tsql_title) { 'user-permissions-MSSQLSERVER-loggingDb-loggingUser-GRANT' } + let(:params) { { + :user => 'loggingUser', + :permissions => ['SELECT'], + :database => 'loggingDb', + } } + end + %w(revoke grant deny).each do |state| + context "state => '#{state}'" do + let(:sqlserver_tsql_title) { "user-permissions-MSSQLSERVER-loggingDb-loggingUser-#{state.upcase}" } + let(:should_contain_command) { ["#{state.upcase} SELECT TO [loggingUser];", 'USE [loggingDb];'] } + describe "lowercase #{state}" do + let(:additional_params) { {:state => state} } + it_behaves_like 'sqlserver_tsql command' + end + state.capitalize! + describe "capitalized #{state}" do + let(:additional_params) { {:state => state} } + it_behaves_like 'sqlserver_tsql command' + end + end + end + + context 'permission' do + describe 'upper limit' do + permission =random_string_of_size(128, false) + let(:additional_params) { {:permissions => [permission]} } + let(:sqlserver_tsql_title) { "user-permissions-MSSQLSERVER-loggingDb-loggingUser-GRANT" } + let(:should_contain_command) { ['USE [loggingDb];'] } + it_behaves_like 'sqlserver_tsql command' + end + describe 'alter' do + let(:additional_params) { {:permissions => ['ALTER']} } + let(:should_contain_command) { ['USE [loggingDb];', 'GRANT ALTER TO [loggingUser];'] } + let(:sqlserver_tsql_title) { 'user-permissions-MSSQLSERVER-loggingDb-loggingUser-GRANT' } + it_behaves_like 'sqlserver_tsql command' + end + end + + describe 'Minimal Params' do + let(:pre_condition) { <<-EOF + define sqlserver::config{} + sqlserver::config {'MSSQLSERVER': } + EOF + } + let(:should_contain_command) { ['USE [loggingDb];'] } + it_behaves_like 'compile' + end + + context 'with_grant_option =>' do + describe 'true' do + let(:sqlserver_tsql_title) { 'user-permissions-MSSQLSERVER-loggingDb-loggingUser-GRANT-WITH_GRANT_OPTION' } + let(:additional_params) { {:with_grant_option => true} } + let(:should_contain_command) { [ + "IF @perm_state != 'GRANT_WITH_GRANT_OPTION'", + 'GRANT SELECT TO [loggingUser] WITH GRANT OPTION;', + ] } + let(:should_not_contain_command) { [ + 'REVOKE GRANT OPTION FOR SELECT FROM [loggingUser];'] } + let(:should_contain_onlyif) { ["IF @perm_state != 'GRANT_WITH_GRANT_OPTION'",] } + it_behaves_like 'sqlserver_tsql command' + it_behaves_like 'sqlserver_tsql without_command' + it_behaves_like 'sqlserver_tsql onlyif' + end + describe 'false' do + let(:should_contain_command) { [ + "IF @perm_state != 'GRANT'", + 'GRANT SELECT TO [loggingUser];', + 'REVOKE GRANT OPTION FOR SELECT TO [loggingUser] CASCADE;', + "IF 'GRANT_WITH_GRANT_OPTION' = ISNULL(", + ] } + let(:should_contain_onlyif) { ["IF @perm_state != 'GRANT'",] } + it_behaves_like 'sqlserver_tsql command' + it_behaves_like 'sqlserver_tsql onlyif' + end + end + end + + context 'command syntax' do + include_context 'manifests' do + let(:title) { 'myTitle' } + let(:sqlserver_tsql_title) { 'user-permissions-MSSQLSERVER-loggingDb-loggingUser-GRANT' } + let(:params) { { + :user => 'loggingUser', + :permissions => ['SELECT'], + :database => 'loggingDb', + } } + describe '' do + let(:should_contain_command) { [ + 'USE [loggingDb];', + 'GRANT SELECT TO [loggingUser];', + /DECLARE @perm_state varchar\(250\), @error_msg varchar\(250\)/, + /SET @permission = 'SELECT'/, + /SET @perm_state = ISNULL\(\n\s+\(SELECT perm.state_desc FROM sys\.database_principals princ\n\s+JOIN sys\./, + /JOIN sys\.database_permissions perm ON perm\.grantee_principal_id = princ.principal_id\n\s+WHERE/, + /WHERE princ\.type in \('U','S','G'\) AND name = 'loggingUser' AND permission_name = @permission\),\n\s+'REVOKE'\)\s+;/, + /SET @error_msg = 'EXPECTED user \[loggingUser\] to have permission \[' \+ @permission \+ '\] with GRANT but got ' \+ @perm_state;/, + /IF @perm_state != 'GRANT'\n\s+THROW 51000, @error_msg, 10/ + ] } + it_behaves_like 'sqlserver_tsql command' + end + end + end + +end diff --git a/spec/defines/user_spec.rb b/spec/defines/user_spec.rb new file mode 100644 index 00000000..54b77183 --- /dev/null +++ b/spec/defines/user_spec.rb @@ -0,0 +1,188 @@ +require 'spec_helper' +require File.expand_path(File.join(File.dirname(__FILE__), 'manifest_shared_examples.rb')) + +RSpec.describe 'sqlserver::user', :type => :define do + include_context 'manifests' do + let(:title) { 'loggingUser' } + let(:sqlserver_tsql_title) { 'user-MSSQLSERVER-myDatabase-loggingUser' } + let(:params) { {:user => 'loggingUser', :database => 'myDatabase'} } + end + + describe 'should fail when password above 128 characters' do + o = [('a'..'z'), ('A'..'Z'), (0..9)].map { |i| i.to_a }.flatten + string = (0...129).map { o[rand(o.length)] }.join + let(:additional_params) { {:password => string} } + let(:raise_error_check) { 'Password must be equal or less than 128 characters' } + it_should_behave_like 'validation error' + end + + describe 'should fail when database above 128 characters' do + o = [('a'..'z'), ('A'..'Z'), (0..9)].map { |i| i.to_a }.flatten + string = (0...129).map { o[rand(o.length)] }.join + let(:additional_params) { {:database => string} } + let(:raise_error_check) { 'Database name must be between 1 and 128 characters' } + let(:sqlserver_tsql_title) { "user-MSSQLSERVER-#{string}-loggingUser" } + it_should_behave_like 'validation error' + end + + describe 'should contain correct sql syntax for check' do + let(:should_contain_onlyif) { [ + "USE [myDatabase]", + "\nIF NOT EXISTS(SELECT name FROM sys.database_principals WHERE type in ('U','S','G') AND name = 'loggingUser')\n", + "THROW 51000, 'User [loggingUser] does not exist for database [myDatabase]', 10\n" + ] } + let(:should_contain_command) { [ + "USE [myDatabase]", + /CREATE USER \[loggingUser\]\n\s+FROM LOGIN \[mySysLogin\]/ + ] } + let(:should_not_contain_command) { [ + 'PASSWORD', + 'DEFAULT_SCHEMA', + 'WITH' + ] } + let(:additional_params) { {:login => 'mySysLogin'} } + it_should_behave_like 'sqlserver_tsql onlyif' + it_should_behave_like 'sqlserver_tsql command' + it_should_behave_like 'sqlserver_tsql without_command' + end + + describe 'when a password is specified' do + password = 'Pupp3t1@' + let(:additional_params) { {:password => password} } + let(:should_contain_command) { [ + "USE [myDatabase];", + /CREATE USER \[loggingUser\]\n\s+WITH PASSWORD = '#{password}'/ + ] } + let(:should_not_contain_command) { [ + 'DEFAULT_SCHEMA', + ] } + it_should_behave_like 'sqlserver_tsql onlyif' + it_should_behave_like 'sqlserver_tsql command' + it_should_behave_like 'sqlserver_tsql without_command' + end + + describe 'when a default_schema is specified' do + let(:additional_params) { {:default_schema => 'dbo'} } + let(:should_contain_command) { [ + "USE [myDatabase]", + /CREATE USER \[loggingUser\]\n\s+WITH\s+DEFAULT_SCHEMA = dbo/ + ] } + let(:should_not_contain_command) { [ + 'PASSWORD', + ] } + it_should_behave_like 'sqlserver_tsql command' + it_should_behave_like 'sqlserver_tsql without_command' + end + + describe 'when providing windows user' do + let(:additional_params) { {:user => 'myMachineName/myUser'} } + let(:sqlserver_tsql_title) { 'user-MSSQLSERVER-myDatabase-myMachineName/myUser' } + let(:should_contain_command) { [ + "USE [myDatabase];", + 'CREATE USER [myMachineName/myUser]' + ] } + it_should_behave_like 'sqlserver_tsql command' + end + + describe 'when providing a windows user and login' do + let(:additional_params) { {:user => 'myMachineName/myUser', :login => 'myMachineName/myUser'} } + let(:sqlserver_tsql_title) { 'user-MSSQLSERVER-myDatabase-myMachineName/myUser' } + let(:should_contain_command) { [ + "USE [myDatabase]", + /CREATE USER \[myMachineName\/myUser\]\n\s+FROM LOGIN \[myMachineName\/myUser\]/ + ] } + it_should_behave_like 'sqlserver_tsql command' + end + + describe 'have dependency on Sqlserver::Config[MSSQLSERVER]' do + it 'should require ::config' do + should contain_sqlserver_tsql(sqlserver_tsql_title).with_require('Sqlserver::Config[MSSQLSERVER]') + end + end + + describe 'when ensure => absent' do + let(:additional_params) { {:ensure => 'absent'} } + let(:sqlserver_contain_command) { [ + 'USE [loggingDb];\nDROP [loggingUser]', + "\nIF EXISTS(SELECT name FROM sys.database_principals WHERE name = 'loggingUser')\n THROW", + ] } + let(:sqlserver_contain_onlyif) { [ + "\nIF EXISTS(SELECT name FROM sys.database_principals WHERE type in ('U','S','G') AND name = 'loggingUser')\n", + ] } + it_should_behave_like 'sqlserver_tsql command' + it_should_behave_like 'sqlserver_tsql onlyif' + end + context 'permissions =>' do + let(:title) { 'myTitle' } + let(:params) { {:user => 'loggingUser', :database => 'myDatabase'} } + let(:permissions) { {} } + shared_examples 'sqlserver_user_permissions exists' do |type| + it { + params[:permissions] = permissions + type_title = (type =~ /GRANT_WITH_OPTION/i ? 'GRANT-WITH_GRANT_OPTION' : type.upcase) + should contain_sqlserver__user__permissions("Sqlserver::User[#{title}]-#{type_title}-loggingUser").with( + { + 'user' => 'loggingUser', + 'database' => 'myDatabase', + 'state' => type == 'GRANT_WITH_OPTION' ? 'GRANT' : type.upcase, + 'with_grant_option' => type == 'GRANT_WITH_OPTION', + 'permissions' => permissions[type], + 'require' => 'Sqlserver_tsql[user-MSSQLSERVER-myDatabase-loggingUser]' + } + ) + } + end + + shared_examples 'sqlserver_user_permissions absent' do |type| + it { + params[:permissions] = permissions + type_title = (type =~ /GRANT_WITH_OPTION/i ? 'GRANT-WITH_GRANT_OPTION' : type.upcase) + should_not contain_sqlserver__user__permissions("Sqlserver::User[#{title}]-#{type_title}-loggingUser") + } + end + + describe 'GRANT permissions' do + let(:permissions) { {'GRANT' => ['SELECT']} } + it_behaves_like 'sqlserver_user_permissions exists', 'GRANT' + it_behaves_like 'sqlserver_user_permissions absent', 'DENY' + it_behaves_like 'sqlserver_user_permissions absent', 'REVOKE' + it_behaves_like 'sqlserver_user_permissions absent', 'GRANT_WITH_OPTION' + end + + describe 'GRANT DENY' do + let(:permissions) { {'GRANT' => ['CONNECT SQL'], 'DENY' => ['INSERT']} } + it_behaves_like 'sqlserver_user_permissions exists', 'GRANT' + it_behaves_like 'sqlserver_user_permissions exists', 'DENY' + it_behaves_like 'sqlserver_user_permissions absent', 'REVOKE' + it_behaves_like 'sqlserver_user_permissions absent', 'GRANT_WITH_OPTION' + end + + describe 'GRANT_WITH_OPTION' do + let(:permissions) { {'GRANT_WITH_OPTION' => ['CONNECT SQL']} } + it_behaves_like 'sqlserver_user_permissions exists', 'GRANT_WITH_OPTION' + end + + describe 'REVOKE' do + let(:permissions) { {'revoke' => ['CREATE ANY DATABASE']} } + it_behaves_like 'sqlserver_user_permissions exists', 'revoke' + it_behaves_like 'sqlserver_user_permissions absent', 'GRANT' + it_behaves_like 'sqlserver_user_permissions absent', 'DENY' + it_behaves_like 'sqlserver_user_permissions absent', 'GRANT_WITH_OPTION' + end + + describe 'empty' do + %w(GRANT DENY REVOKE GRANT-WITH_GRANT_OPTION).each do |type| + it_behaves_like 'sqlserver_user_permissions absent', type + end + end + + describe 'duplicate permissions' do + let(:additional_params) { { + :permissions => {'GRANT' => ['CONNECT SQL'], 'REVOKE' => ['CONNECT SQL']} + } } + let(:raise_error_check) { "Duplicate permissions found for sqlserver::user[#{title}" } + let(:raise_error_check) { "Duplicate permissions found for sqlserver::user[#{title}" } + it_behaves_like 'validation error' + end + end +end diff --git a/spec/functions/sqlserver_upcase_spec.rb b/spec/functions/sqlserver_upcase_spec.rb new file mode 100644 index 00000000..47312687 --- /dev/null +++ b/spec/functions/sqlserver_upcase_spec.rb @@ -0,0 +1,54 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the sqlserver_upcase function" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + expect(Puppet::Parser::Functions.function("sqlserver_upcase")).to eq("function_sqlserver_upcase") + end + + it "should upcase a string" do + result = scope.function_sqlserver_upcase(["abc"]) + expect(result).to(eq('ABC')) + end + + it "should do nothing if a string is already upcase" do + result = scope.function_sqlserver_upcase(["ABC"]) + expect(result).to(eq('ABC')) + end + + it "should accept objects which extend String" do + class AlsoString < String + end + + value = AlsoString.new('abc') + result = scope.function_sqlserver_upcase([value]) + result.should(eq('ABC')) + end + + it 'should accept hashes and return uppercase' do + expect( + scope.function_sqlserver_upcase([{'test' => %w(this that and other thing)}]) + ).to eq({'TEST' => %w(THIS THAT AND OTHER THING)}) + end + + if :test.respond_to?(:upcase) + it 'should accept hashes of symbols' do + expect( + scope.function_sqlserver_upcase([{:test => [:this, :that, :other]}]) + ).to eq({:TEST => [:THIS, :THAT, :OTHER]}) + end + it 'should return upcase symbol' do + expect( + scope.function_sqlserver_upcase([:test]) + ).to eq(:TEST) + end + it 'should return mixed objects in upcease' do + expect( + scope.function_sqlserver_upcase([[:test, 'woot']]) + ).to eq([:TEST, 'WOOT']) + + end + end +end diff --git a/spec/functions/sqlserver_validate_hash_uniq_values_spec.rb b/spec/functions/sqlserver_validate_hash_uniq_values_spec.rb new file mode 100644 index 00000000..68485600 --- /dev/null +++ b/spec/functions/sqlserver_validate_hash_uniq_values_spec.rb @@ -0,0 +1,28 @@ +#! /usr/bin/env ruby -S rspec +require 'spec_helper' + +describe "the sqlserver_validate_hash_uniq_values" do + let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + + it "should exist" do + expect(Puppet::Parser::Functions.function("sqlserver_validate_hash_uniq_values")).to eq("function_sqlserver_validate_hash_uniq_values") + end + + it "should accept mixed value types of string and string[]" do + expect { + scope.function_sqlserver_validate_hash_uniq_values([{'test' => 'this', 'and' => ['test', 'this']}]) + }.to raise_error + end + + it "should pass validation" do + expect { + scope.function_sqlserver_validate_hash_uniq_values([{'test' => 'this', 'and' => ['test', 'another']}]) + }.to_not raise_error + end + + it "should require a hash" do + expect { + scope.function_sqlserver_validate_hash_uniq_values(["MyString"]) + }.to raise_error + end +end diff --git a/spec/functions/sqlserver_validate_instance_name_spec.rb b/spec/functions/sqlserver_validate_instance_name_spec.rb index 5e180f03..ab394fd3 100644 --- a/spec/functions/sqlserver_validate_instance_name_spec.rb +++ b/spec/functions/sqlserver_validate_instance_name_spec.rb @@ -2,11 +2,17 @@ describe 'sqlserver_validate_instance_name function' do let(:scope) { PuppetlabsSpec::PuppetInternals.scope } + it 'should exist' do expect(Puppet::Parser::Functions.function("sqlserver_validate_instance_name")).to eq("function_sqlserver_validate_instance_name") end + it 'should fail with over 16 characters' do expect { scope.function_sqlserver_validate_instance_name('ABCDEFGHIJKLMNOPQRSTUVWXYZ') }.to raise_error end + it 'should fail empty string' do + expect { scope.function_sqlserver_validate_instance_name('') }.to raise_error + end + end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ccf6c531..9e1a07e8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,12 +1,12 @@ require 'simplecov' -require 'rspec' - +require 'rspec-puppet' require 'puppetlabs_spec_helper/module_spec_helper' -dir = File.expand_path(File.dirname(__FILE__)) -$LOAD_PATH.unshift File.join(dir, 'lib') -module PuppetSpec - FIXTURE_DIR = File.join(dir = File.expand_path(File.dirname(__FILE__)), "fixtures") unless defined?(FIXTURE_DIR) +fixture_path = File.expand_path(File.join(__FILE__, '..', 'fixtures')) + +RSpec.configure do |c| + c.module_path = File.join(fixture_path, 'modules') + c.manifest_dir = File.join(fixture_path, 'manifests') end SimpleCov.start do diff --git a/spec/unit/puppet/property/tsql_spec.rb b/spec/unit/puppet/property/tsql_spec.rb new file mode 100644 index 00000000..216348d1 --- /dev/null +++ b/spec/unit/puppet/property/tsql_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe 'tsql' do + + before :each do + @node = Puppet::Type.type(:sqlserver_tsql).new(:name => 'update user') + end + + it { + @node[:command] = 'UPDATE [my_login] SET PASSWORD = "MYSillyPassword"' + expect(@node[:command]).to match(/BEGIN TRY/) + expect(@node[:command]).to include('UPDATE [my_login] SET PASSWORD = "MYSillyPassword"') + } + it 'should munge value to have begin and end try' do + @node[:command] = 'function foo' + @node[:onlyif] = 'exec bar' + expect(@node[:onlyif]).to match(/BEGIN TRY\n\s+exec bar\nEND TRY/) + expect(@node[:command]).to match(/BEGIN TRY\n\s+function foo\nEND TRY/) + end + +end diff --git a/spec/unit/puppet_x/sql_connection_spec.rb b/spec/unit/puppet_x/sql_connection_spec.rb new file mode 100644 index 00000000..457df19a --- /dev/null +++ b/spec/unit/puppet_x/sql_connection_spec.rb @@ -0,0 +1,51 @@ +require 'rspec' +require 'spec_helper' +require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'lib/puppet_x/sqlserver/sql_connection')) + +RSpec.describe PuppetX::Sqlserver::SqlConnection do + let(:subject) { PuppetX::Sqlserver::SqlConnection } + + def stub_connection + @connection = mock() + @sql = subject.new + @sql.stubs(:create_connection).returns(@connection) + @sql.stubs(:sql_exception_class).returns(Exception) + end + + describe 'open' do + it 'should not add MSSQLSERVER to connection string' do + stub_connection + @connection.stubs(:Open).with('Provider=SQLOLEDB.1;Persist Security Info=False;User ID=sa;password=Pupp3t1@;Initial Catalog=master;Data Source=localhost;Network Library=dbmssocn') + @sql.open('sa', 'Pupp3t1@', 'MSSQLSERVER') + + end + it 'should add a non default instance to connection string' do + stub_connection + @connection.stubs(:Open).with('Provider=SQLOLEDB.1;Persist Security Info=False;User ID=superuser;password=puppetTested;Initial Catalog=master;Data Source=localhost\LOGGING;Network Library=dbmssocn') + @sql.open('superuser', 'puppetTested', 'LOGGING') + end + end + + describe 'command' do + it 'should not raise an error but populate has_errors' do + stub_connection + @sql.stubs(:sql_exception_class).returns(Exception) + @sql.stubs(:execute).raises(Exception.new('error has happened')) + expect { @sql.command('whacka whacka whacka') }.to_not raise_error(Exception) + expect(@sql.has_errors).to eq(true) + end + it 'should not raise an error but populate error_message' do + stub_connection + @sql.stubs(:execute).raises(Exception.new('error has happened')) + expect { @sql.command('whacka whacka whacka') }.to_not raise_error(Exception) + expect(@sql.error_message).to eq('error has happened') + end + it 'should yield when passed a block' do + stub_connection + @sql.stubs(:execute).returns('results') + @sql.command('myquery') do |r| + expect(r).to eq('results') + end + end + end +end diff --git a/templates/create/database.sql.erb b/templates/create/database.sql.erb index 56119721..1ed52cc9 100644 --- a/templates/create/database.sql.erb +++ b/templates/create/database.sql.erb @@ -1,4 +1,3 @@ -BEGIN TRY USE master DECLARE @default_db_path as nvarchar(max), @default_log_path as varchar(max) @@ -116,5 +115,3 @@ END IF NOT EXISTS (select * from sys.databases WHERE name = '<%= @db_name %>') THROW 51000, 'DATABASE CREATION FAILED', 10 -END TRY -<%= scope.function_template(['sqlserver/snippets/begin_catch.sql.erb']) %> diff --git a/templates/create/login.sql.erb b/templates/create/login.sql.erb index ca683a67..475e3832 100644 --- a/templates/create/login.sql.erb +++ b/templates/create/login.sql.erb @@ -1,4 +1,3 @@ -GO DECLARE @login as varchar(255) = '<%= @login %>', @is_disabled as tinyint = <%= @disabled ? 1 : 0 %>; @@ -47,6 +46,3 @@ BEGIN <% end -%> <% end -%> END -GO - - diff --git a/templates/create/login/permission.sql.erb b/templates/create/login/permission.sql.erb new file mode 100644 index 00000000..eb4c97f2 --- /dev/null +++ b/templates/create/login/permission.sql.erb @@ -0,0 +1,17 @@ +USE [master]; +DECLARE @perm_state varchar(250), @error_msg varchar(250), @permission varchar(250); +<% @permissions.each do |permission| + permission.upcase! +%> +SET @permission = '<%= permission %>' +BEGIN + <% if @with_grant_option == false %> + IF 'GRANT_WITH_GRANT_OPTION' = <%= scope.function_template(['sqlserver/snippets/login/get_perm_state.sql.erb']) %> + REVOKE GRANT OPTION FOR <%= permission %> TO [<%= @login %>] CASCADE; + <% end %> + <%= @_state %> <%= permission %> TO [<%= @login %>]<% if @with_grant_option == true %> WITH GRANT OPTION<% end %>; +END +BEGIN + <%= scope.function_template(['sqlserver/snippets/login/permission/exists.sql.erb']) %> +END +<% end %> diff --git a/templates/create/role.sql.erb b/templates/create/role.sql.erb new file mode 100644 index 00000000..57441b2a --- /dev/null +++ b/templates/create/role.sql.erb @@ -0,0 +1,10 @@ +USE [<%= @database %>]; +BEGIN + <%= scope.function_template(['sqlserver/snippets/role/exists.sql.erb']) %> + CREATE <% if @type == 'SERVER' %>SERVER <% end %>ROLE [<%= @role %>]<% if @authorization %> AUTHORIZATION [<%= @authorization %>]<% end %>; + <% if @authorization %> + <%= scope.function_template(['sqlserver/snippets/role/owner_check.sql.erb']) %> + ALTER AUTHORIZATION ON <% if @type =='SERVER' %>SERVER <% end %>ROLE::[<%= @role %>] TO [<%= @authorization %>]; + <% end %> +END +<%= scope.function_template(['sqlserver/query/role_exists.sql.erb']) %> diff --git a/templates/create/role/members.sql.erb b/templates/create/role/members.sql.erb new file mode 100644 index 00000000..b05e4009 --- /dev/null +++ b/templates/create/role/members.sql.erb @@ -0,0 +1,30 @@ +USE [<%= @database %>]; +DECLARE + @role varchar(128) = '<%= @role %>', + @member varchar(128), + @error_msg varchar(250); + +<%- @members.each do |member| -%> +BEGIN +SET @member = '<%= member %>'; +<%= scope.function_template(['sqlserver/snippets/role/member_exists.sql.erb']) -%> + ALTER <% if @type == 'SERVER' %>SERVER <% end %>ROLE [<%= @role %>] ADD MEMBER [<%= member %>]; + +<%= scope.function_template(['sqlserver/snippets/role/member_exists.sql.erb']) -%> + THROW 51000, 'Failed to add member [<%= member %>] to Role [<%= @role %>]', 10 +END +<% end -%> + +<% if @members_purge %> +<%= scope.function_template(['sqlserver/snippets/role/populate_purge_members.sql.erb']) -%> + +DECLARE @sql varchar(250), @row int = 1, @row_count int; +SET @row_count = (SELECT COUNT(*) FROM @purge_members); + +WHILE(@row <= @row_count) +BEGIN + SET @sql = 'ALTER <% if @type == 'SERVER' %>SERVER <% end %>ROLE [<%= @role %>] DROP MEMBER [' + (SELECT member FROM @purge_members WHERE ID = @row) + '];' + EXEC(@sql) + SET @row += 1 +END +<% end %> diff --git a/templates/create/role/permissions.sql.erb b/templates/create/role/permissions.sql.erb new file mode 100644 index 00000000..42637064 --- /dev/null +++ b/templates/create/role/permissions.sql.erb @@ -0,0 +1,15 @@ +USE [<%= @database %>]; +<%= scope.function_template(['sqlserver/snippets/role/declare_and_set_variables.sql.erb']) -%> + + <%- @_upermissions.each do |permission| + permission.upcase! + -%> + SET @permission = '<%= permission %>'; + <% if @with_grant_option == false %> + IF 'GRANT_WITH_GRANT_OPTION' = <%= scope.function_template(['sqlserver/snippets/principal/permission/get_perm_state.sql.erb']) -%> + BEGIN + REVOKE GRANT OPTION FOR <%= permission %> TO [<%= @role %>] CASCADE; + END + <% end -%> + <%= @_state %> <%= permission %> TO [<%= @role %>]<% if @with_grant_option == true %> WITH GRANT OPTION<% end %>; + <% end %> diff --git a/templates/create/sp_configure.sql.erb b/templates/create/sp_configure.sql.erb index 541a46f3..496b4ba1 100644 --- a/templates/create/sp_configure.sql.erb +++ b/templates/create/sp_configure.sql.erb @@ -1,4 +1,3 @@ -BEGIN TRY DECLARE @return_value INT EXECUTE @return_value = sp_configure @configname = N'<%= @config_name %>', @configvalue = <%= @value %> IF @return_value != 0 @@ -7,5 +6,3 @@ IF @return_value != 0 ELSE RECONFIGURE <% if @with_override %>WITH OVERRIDE<% end %> <% end -%> -END TRY -<%= scope.function_template(['sqlserver/snippets/begin_catch.sql.erb']) %> diff --git a/templates/create/user.sql.erb b/templates/create/user.sql.erb new file mode 100644 index 00000000..b92616e3 --- /dev/null +++ b/templates/create/user.sql.erb @@ -0,0 +1,18 @@ +USE [<%= @database %>]; +<% if @password %> + IF EXISTS(select containment from sys.databases WHERE name = '<%= @database %>' AND containment = 0) + THROW 51000, 'Database must be contained in order to use passwords', 10 +<% end %> +CREATE USER [<%= @user %>] +<% if @login -%> + FROM LOGIN [<%= @login %>] +<% else -%> + <% if @password -%> + WITH PASSWORD = '<%= @password %>' + <% end -%> +<% end -%> +<% if @default_schema -%> + <% if @password -%>,<% else -%> + WITH <% end -%> + DEFAULT_SCHEMA = <%= @default_schema %> +<% end -%> diff --git a/templates/create/user/permission.sql.erb b/templates/create/user/permission.sql.erb new file mode 100644 index 00000000..22307807 --- /dev/null +++ b/templates/create/user/permission.sql.erb @@ -0,0 +1,17 @@ + USE [<%= @database %>]; + DECLARE @perm_state varchar(250), @error_msg varchar(250), @permission varchar(250); + <% @permissions.each do |permission| + permission.upcase! + %> +SET @permission = '<%= permission %>' +BEGIN + <% if @with_grant_option == false %> + IF 'GRANT_WITH_GRANT_OPTION' = <%= scope.function_template(['sqlserver/snippets/user/permission/get_perm_state.sql.erb']) %> + REVOKE GRANT OPTION FOR <%= permission %> TO [<%= @user %>] CASCADE; + <% end %> + <%= @_state %> <%= permission %> TO [<%= @user %>]<% if @with_grant_option == true %> WITH GRANT OPTION<% end %>; +END +BEGIN + <%= scope.function_template(['sqlserver/snippets/user/permission/exists.sql.erb']) %> +END +<% end %> diff --git a/templates/delete/role.sql.erb b/templates/delete/role.sql.erb new file mode 100644 index 00000000..925fe6d3 --- /dev/null +++ b/templates/delete/role.sql.erb @@ -0,0 +1,5 @@ +USE [<%= @database %>]; +BEGIN + DROP <% if @type == 'SERVER' %>SERVER <% end %>ROLE [<%= @role %>]; +END +<%= scope.function_template(['sqlserver/query/role_exists.sql.erb']) %> diff --git a/templates/delete/user.sql.erb b/templates/delete/user.sql.erb new file mode 100644 index 00000000..fffdbe7d --- /dev/null +++ b/templates/delete/user.sql.erb @@ -0,0 +1,4 @@ +USE [<%= @database %>]; +DROP USER [<%= @user %>]; +IF EXISTS(SELECT name FROM sys.database_principals WHERE name = '<%= @user %>') + THROW 51000, 'Failed to drop user <%= @user %>', 10 diff --git a/templates/instance_config.erb b/templates/instance_config.erb index a0defafd..de5d3888 100644 --- a/templates/instance_config.erb +++ b/templates/instance_config.erb @@ -1 +1 @@ -{ 'instance': '<%= @instance_name %>','admin':'<%= @admin_user %>','pass':'<%= @admin_pass %>' } +{ "instance": "<%= @instance_name %>","admin":"<%= @admin_user %>","pass":"<%= @admin_pass %>" } diff --git a/templates/query/database_exists.sql.erb b/templates/query/database_exists.sql.erb index d329f2ee..640dbe2d 100644 --- a/templates/query/database_exists.sql.erb +++ b/templates/query/database_exists.sql.erb @@ -1,4 +1,3 @@ -BEGIN TRY -- QUICK CHECK before most costly query IF <% if @ensure == 'present' %>NOT<% end %> EXISTS(SELECT name from sys.databases WHERE name = '<%= @db_name %>') THROW 51000, 'The database does <% if @ensure == 'present' %>not<% end %> exist', 10 @@ -23,5 +22,3 @@ IF <% if @ensure == 'present' %>NOT<% end %> EXISTS(SELECT name from sys.databas <% end #end ensure present section -%> -END TRY -<%= scope.function_template(['sqlserver/snippets/begin_catch.sql.erb']) %> diff --git a/templates/query/login/permission_exists.sql.erb b/templates/query/login/permission_exists.sql.erb new file mode 100644 index 00000000..d6023cda --- /dev/null +++ b/templates/query/login/permission_exists.sql.erb @@ -0,0 +1,8 @@ +USE [master]; +DECLARE @perm_state varchar(250), @error_msg varchar(250), @permission varchar(250); + <% @permissions.each do |permission| + permission.upcase! + %> +SET @permission = '<%= permission %>' +<%= scope.function_template(['sqlserver/snippets/login/permission/exists.sql.erb']) %> +<% end %> diff --git a/templates/query/login_exists.sql.erb b/templates/query/login_exists.sql.erb index c2eb63cb..897b002a 100644 --- a/templates/query/login_exists.sql.erb +++ b/templates/query/login_exists.sql.erb @@ -1,5 +1,3 @@ - -GO DECLARE @login as varchar(255) = '<%= @login %>', @is_disabled as tinyint = <%= @disabled ? 1 : 0 %>, @@ -9,7 +7,7 @@ DECLARE @default_db as varchar(255) = '<%= @default_database %>', @default_lang as varchar(50) = '<%= @default_language %>' IF <% if @ensure == 'present' %>NOT<% end %> EXISTS(SELECT name FROM sys.server_principals WHERE name = '<%= @login %>') - THROW 51000, 'ERROR: the login is not <%= @ensure %>', 10 + THROW 51000, 'ERROR: The login is not <%= @ensure %>', 10 <% if @ensure == 'present' %> BEGIN @@ -26,7 +24,7 @@ IF NOT EXISTS( AND is_policy_checked = @check_policy AND is_expiration_checked = @check_expiration <% end %> - ) THROW 51000, 'ERROR: the login is not in the correct state', 10 + ) THROW 51000, 'ERROR: The login is not in the correct state', 10 /* If it does exist check for each role is in the correct state */ <% @svrroles.each do |role, enable_bit| %> IF (SELECT COUNT(me.role_principal_id) from sys.server_role_members me @@ -40,4 +38,3 @@ IF NOT EXISTS( <% end %> END - diff --git a/templates/query/role/member_exists.sql.erb b/templates/query/role/member_exists.sql.erb new file mode 100644 index 00000000..6072a0da --- /dev/null +++ b/templates/query/role/member_exists.sql.erb @@ -0,0 +1,18 @@ +USE [<%= @database %>]; +DECLARE + @role varchar(128) = '<%= @role %>', + @member varchar(128), + @error_msg varchar(250); + +<% @members.each do |member| %> +SET @member = '<%= member %>'; +SET @error_msg = 'The member [<%= member %>] is <% if @ensure == 'present'%>not <% end %>a member of the role [<%=@role %>]'; +<%= scope.function_template(['sqlserver/snippets/role/member_exists.sql.erb']) -%> + THROW 51000, @error_msg, 10 +<% end %> + +<% if @members_purge %> +<%= scope.function_template(['sqlserver/snippets/role/populate_purge_members.sql.erb']) %> +IF 0 != (SELECT COUNT(*) FROM @purge_members) + THROW 51000, 'Unlisted Members in Role, will be purged', 10 +<% end %> diff --git a/templates/query/role/permission_exists.sql.erb b/templates/query/role/permission_exists.sql.erb new file mode 100644 index 00000000..cd1feb22 --- /dev/null +++ b/templates/query/role/permission_exists.sql.erb @@ -0,0 +1,8 @@ +USE [<%= @database %>]; +<%= scope.function_template(['sqlserver/snippets/role/declare_and_set_variables.sql.erb']) -%> + +<% @permissions.each do |permission| + permission.upcase! -%> +SET @permission = '<%= permission %>'; +<%= scope.function_template(['sqlserver/snippets/principal/permission/exists.sql.erb']) -%> +<% end -%> diff --git a/templates/query/role_exists.sql.erb b/templates/query/role_exists.sql.erb new file mode 100644 index 00000000..61ed61a0 --- /dev/null +++ b/templates/query/role_exists.sql.erb @@ -0,0 +1,7 @@ +USE [<%= @database %>]; +<%= scope.function_template(['sqlserver/snippets/role/exists.sql.erb']) %> + THROW 51000, 'The <%= @type %> ROLE [<%= @role %>] does <% if @ensure == 'present' %>not<% end %> exist', 10 +<% if @ensure == 'present' && @authorization -%> + <%= scope.function_template(['sqlserver/snippets/role/owner_check.sql.erb']) %> + THROW 51000, 'The <%= @type %> ROLE [<%= @role %>] does not have the correct owner of [<%= @authorization %>]', 10 +<% end -%> diff --git a/templates/query/sp_configure.sql.erb b/templates/query/sp_configure.sql.erb index fd857320..0547d315 100644 --- a/templates/query/sp_configure.sql.erb +++ b/templates/query/sp_configure.sql.erb @@ -1,4 +1,3 @@ -BEGIN TRY USE master; DECLARE @sp_conf TABLE ( @@ -11,5 +10,3 @@ DECLARE @sp_conf TABLE INSERT INTO @sp_conf EXECUTE sp_configure @configname = N'<%= @config_name %>' IF EXISTS(select * from @sp_conf where name = '<%= @config_name %>' AND run_value != <%= @value %>) THROW 51000, 'sp_configure `<%= @config_name %>` is not in the correct state', 10 -END TRY -<%= scope.function_template(['sqlserver/snippets/begin_catch.sql.erb']) %> diff --git a/templates/query/user/permission_exists.sql.erb b/templates/query/user/permission_exists.sql.erb new file mode 100644 index 00000000..cae4adae --- /dev/null +++ b/templates/query/user/permission_exists.sql.erb @@ -0,0 +1,8 @@ +USE [<%= @database %>]; + +DECLARE @perm_state varchar(250), @error_msg varchar(250), @permission varchar(250); +<% @permissions.each do |permission| +permission.upcase! %> +SET @permission = '<%= permission %>' +<%= scope.function_template(['sqlserver/snippets/user/permission/exists.sql.erb']) %> +<% end %> diff --git a/templates/query/user_exists.sql.erb b/templates/query/user_exists.sql.erb new file mode 100644 index 00000000..7fa0af5a --- /dev/null +++ b/templates/query/user_exists.sql.erb @@ -0,0 +1,4 @@ +-- Need to use exec instead of use statement as this will trigger try catch +USE [<%= @database %>]; +IF <% if @ensure == 'present' %>NOT<% end %> EXISTS(SELECT name FROM sys.database_principals WHERE type in ('U','S','G') AND name = '<%= @user %>') + THROW 51000, 'User [<%= @user %>] does not exist for database [<%= @database %>]', 10 diff --git a/templates/snippets/begin_catch.sql.erb b/templates/snippets/begin_catch.sql.erb deleted file mode 100644 index 74d8ef42..00000000 --- a/templates/snippets/begin_catch.sql.erb +++ /dev/null @@ -1,5 +0,0 @@ -BEGIN CATCH - DECLARE @msg as VARCHAR(max); - SELECT @msg = 'ERROR: ' + ERROR_MESSAGE(); - THROW 51000, @msg, 10 -END CATCH diff --git a/templates/snippets/login/get_perm_state.sql.erb b/templates/snippets/login/get_perm_state.sql.erb new file mode 100644 index 00000000..82b3b4a3 --- /dev/null +++ b/templates/snippets/login/get_perm_state.sql.erb @@ -0,0 +1,7 @@ +ISNULL( + (SELECT perm.state_desc FROM sys.server_permissions perm + JOIN sys.server_principals princ ON princ.principal_id = perm.grantee_principal_id + WHERE princ.type IN ('U','S','G') + AND princ.name = '<%= @login %>' + AND perm.permission_name = @permission), + 'REVOKE') diff --git a/templates/snippets/login/permission/exists.sql.erb b/templates/snippets/login/permission/exists.sql.erb new file mode 100644 index 00000000..b0ab2ae3 --- /dev/null +++ b/templates/snippets/login/permission/exists.sql.erb @@ -0,0 +1,4 @@ +SET @perm_state = <%= scope.function_template(['sqlserver/snippets/login/get_perm_state.sql.erb']) %>; +SET @error_msg = 'EXPECTED login [<%= @login %>] to have permission [' + @permission + '] with <%= @_state %> but got ' + @perm_state; +IF @perm_state != '<% if @with_grant_option == true %>GRANT_WITH_GRANT_OPTION<% else %><%= @_state %><% end %>' + THROW 51000, @error_msg, 10; diff --git a/templates/snippets/principal/permission/exists.sql.erb b/templates/snippets/principal/permission/exists.sql.erb new file mode 100644 index 00000000..6d609a4e --- /dev/null +++ b/templates/snippets/principal/permission/exists.sql.erb @@ -0,0 +1,4 @@ +SET @perm_state = <%= scope.function_template(['sqlserver/snippets/principal/permission/get_perm_state.sql.erb']) -%>; +SET @error_msg = 'EXPECTED [' + @princ_name + '] to have permission [' + @permission + '] with ' + @state_desc + ' but got ' + @perm_state; +IF @perm_state != @state_desc + THROW 51000, @error_msg, 10; diff --git a/templates/snippets/principal/permission/get_perm_state.sql.erb b/templates/snippets/principal/permission/get_perm_state.sql.erb new file mode 100644 index 00000000..73d0ec30 --- /dev/null +++ b/templates/snippets/principal/permission/get_perm_state.sql.erb @@ -0,0 +1,6 @@ +ISNULL( + (SELECT state_desc FROM sys.<%= @type.downcase %>_permissions prem + JOIN sys.<%= @type.downcase %>_principals r ON r.principal_id = prem.grantee_principal_id + WHERE r.name = @princ_name AND r.type_desc = @princ_type + AND prem.permission_name = @permission), + 'REVOKE') diff --git a/templates/snippets/role/declare_and_set_variables.sql.erb b/templates/snippets/role/declare_and_set_variables.sql.erb new file mode 100644 index 00000000..b92fad95 --- /dev/null +++ b/templates/snippets/role/declare_and_set_variables.sql.erb @@ -0,0 +1,11 @@ +DECLARE + @perm_state varchar(250), + @error_msg varchar(250), + @permission varchar(250), + @princ_name varchar(50), + @princ_type varchar(50), + @state_desc varchar(50); + +SET @princ_type = '<%= @type.upcase %>_ROLE'; +SET @princ_name = '<%= @role %>'; +SET @state_desc = '<% if @with_grant_option == true %>GRANT_WITH_GRANT_OPTION<% else %><%= @_state %><% end %>'; diff --git a/templates/snippets/role/exists.sql.erb b/templates/snippets/role/exists.sql.erb new file mode 100644 index 00000000..e3c7a18b --- /dev/null +++ b/templates/snippets/role/exists.sql.erb @@ -0,0 +1,3 @@ +IF <% if @ensure == 'present' %>NOT <% end %>EXISTS( + SELECT name FROM sys.<%= @type.downcase %>_principals WHERE type_desc = '<%= @type %>_ROLE' AND name = '<%= @role %>' +) diff --git a/templates/snippets/role/member_exists.sql.erb b/templates/snippets/role/member_exists.sql.erb new file mode 100644 index 00000000..124dad35 --- /dev/null +++ b/templates/snippets/role/member_exists.sql.erb @@ -0,0 +1,5 @@ +IF NOT EXISTS ( + SELECT r.name [Role], m.name [Member] FROM sys.<%= @type.downcase %>_role_members rm + JOIN sys.<%= @type.downcase %>_principals r ON rm.role_principal_id = r.principal_id + JOIN sys.<%= @type.downcase %>_principals m ON rm.member_principal_id = m.principal_id + WHERE r.name = @role AND m.name = @member) diff --git a/templates/snippets/role/owner_check.sql.erb b/templates/snippets/role/owner_check.sql.erb new file mode 100644 index 00000000..1bc9a194 --- /dev/null +++ b/templates/snippets/role/owner_check.sql.erb @@ -0,0 +1,4 @@ +IF NOT EXISTS( + SELECT p.name,r.name FROM sys.<%= @type.downcase %>_principals r + JOIN sys.<%= @type.downcase %>_principals p ON p.principal_id = r.owning_principal_id + WHERE r.type_desc = '<%= @type.upcase %>_ROLE' AND p.name = '<%= @authorization %>' AND r.name = '<%= @role %>') diff --git a/templates/snippets/role/populate_purge_members.sql.erb b/templates/snippets/role/populate_purge_members.sql.erb new file mode 100644 index 00000000..f6d18dcc --- /dev/null +++ b/templates/snippets/role/populate_purge_members.sql.erb @@ -0,0 +1,11 @@ +DECLARE @purge_members TABLE ( +ID int IDENTITY(1,1), +member varchar(128) +) +INSERT INTO @purge_members (member) ( +SELECT m.name FROM sys.<%= @type.downcase %>_role_members rm + JOIN sys.<%= @type.downcase %>_principals r ON rm.role_principal_id = r.principal_id + JOIN sys.<%= @type.downcase %>_principals m ON rm.member_principal_id = m.principal_id + WHERE r.name = '<%= @role %>' + <% if !@members.empty? %>AND m.name NOT IN (<%= @members.collect{|m| "'#{m}'"}.join(',') %>)<% end %> + ); diff --git a/templates/snippets/user/permission/exists.sql.erb b/templates/snippets/user/permission/exists.sql.erb new file mode 100644 index 00000000..fba5c4de --- /dev/null +++ b/templates/snippets/user/permission/exists.sql.erb @@ -0,0 +1,4 @@ +SET @perm_state = <%= scope.function_template(['sqlserver/snippets/user/permission/get_perm_state.sql.erb']) %>; +SET @error_msg = 'EXPECTED user [<%= @user %>] to have permission [' + @permission + '] with <%= @_state %> but got ' + @perm_state; +IF @perm_state != '<% if @with_grant_option == true %>GRANT_WITH_GRANT_OPTION<% else %><%= @_state %><% end %>' + THROW 51000, @error_msg, 10 diff --git a/templates/snippets/user/permission/get_perm_state.sql.erb b/templates/snippets/user/permission/get_perm_state.sql.erb new file mode 100644 index 00000000..2e92789d --- /dev/null +++ b/templates/snippets/user/permission/get_perm_state.sql.erb @@ -0,0 +1,5 @@ +ISNULL( + (SELECT perm.state_desc FROM sys.database_principals princ + JOIN sys.database_permissions perm ON perm.grantee_principal_id = princ.principal_id + WHERE princ.type in ('U','S','G') AND name = '<%= @user %>' AND permission_name = @permission), + 'REVOKE')