From 0b97c1d3d001095c8164ef5413bde002fa364a6a Mon Sep 17 00:00:00 2001 From: Travis Fields Date: Fri, 20 Feb 2015 14:52:54 -0800 Subject: [PATCH 1/2] FM-1556 Add ability to manage login server level permissions --- manifests/login/permission.pp | 51 +++++++ spec/defines/login/permission_spec.rb | 135 ++++++++++++++++++ templates/create/login/permission.sql.erb | 11 ++ .../query/login/permission_exists.sql.erb | 8 ++ .../snippets/login/get_perm_state.sql.erb | 7 + 5 files changed, 212 insertions(+) create mode 100644 manifests/login/permission.pp create mode 100644 spec/defines/login/permission_spec.rb create mode 100644 templates/create/login/permission.sql.erb create mode 100644 templates/query/login/permission_exists.sql.erb create mode 100644 templates/snippets/login/get_perm_state.sql.erb diff --git a/manifests/login/permission.pp b/manifests/login/permission.pp new file mode 100644 index 00000000..c66a66b0 --- /dev/null +++ b/manifests/login/permission.pp @@ -0,0 +1,51 @@ +## +# == Define Resource Type: sqlserver::login::permission# +# +# === 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. +# +# [permission] +# The permission 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::permission ( + $login, + $permission = $title, + $state = 'GRANT', + $with_grant_option = false, + $instance = 'MSSQLSERVER', +){ + sqlserver_validate_instance_name($instance) + +## Validate Permissions + $_permission = upcase($permission) + sqlserver_validate_range($_permission, 4, 128, 'Permission must be between 4 and 128 characters') + validate_re($_permission, '^([A-Z]|\s)+$', 'Permissions must be alphabetic only') + + 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) + + sqlserver_tsql{ "login-permission-${instance}-${login}-${_permission}": + 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/spec/defines/login/permission_spec.rb b/spec/defines/login/permission_spec.rb new file mode 100644 index 00000000..bfc09bb1 --- /dev/null +++ b/spec/defines/login/permission_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::permission' do + let(:facts) { {:osfamily => 'windows'} } + context 'validation errors' do + include_context 'manifests' do + let(:title) { 'myTitle' } + let(:sqlserver_tsql_title) { 'login-permission-MSSQLSERVER-loggingUser-SELECT' } + end + context 'login =>' do + let(:params) { { + :permission => '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::Permission[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) { {:permission => ''} } + it_behaves_like 'validation error' + end + describe 'under limit' do + let(:additional_params) { {:permission => random_string_of_size(3, false)} } + it_behaves_like 'validation error' + end + describe 'over limit' do + let(:additional_params) { {:permission => random_string_of_size(129, false)} } + it_behaves_like 'validation error' + end + end + context 'state =>' do + let(:params) { { + :permission => '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-SELECT' } + let(:params) { { + :login => 'loggingUser', + :permission => 'SELECT', + } } + end + %w(revoke grant deny).each do |state| + context "state => '#{state}'" do + let(:sqlserver_tsql_title) { "login-permission-MSSQLSERVER-loggingUser-SELECT" } + 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) { {:permission => permission} } + let(:sqlserver_tsql_title) { "login-permission-MSSQLSERVER-loggingUser-#{permission.upcase}" } + let(:should_contain_command) { ['USE [master];'] } + it_behaves_like 'sqlserver_tsql command' + end + describe 'alter' do + let(:additional_params) { {:permission => 'ALTER'} } + let(:should_contain_command) { ['USE [master];', 'GRANT ALTER TO [loggingUser];'] } + let(:sqlserver_tsql_title) { "login-permission-MSSQLSERVER-loggingUser-ALTER" } + 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-SELECT' } + let(:params) { { + :login => 'loggingUser', + :permission => '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 = 'SELECT'\),\n\s+'REVOKE'\)/, + /DECLARE @error_msg varchar\(250\);\nSET @error_msg = 'EXPECTED login \[loggingUser\] to have permission \[SELECT\] 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/templates/create/login/permission.sql.erb b/templates/create/login/permission.sql.erb new file mode 100644 index 00000000..d17c34e4 --- /dev/null +++ b/templates/create/login/permission.sql.erb @@ -0,0 +1,11 @@ +BEGIN + USE [master]; + <% 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/query/login/permission_exists.sql.erb']) %> +END diff --git a/templates/query/login/permission_exists.sql.erb b/templates/query/login/permission_exists.sql.erb new file mode 100644 index 00000000..7f333128 --- /dev/null +++ b/templates/query/login/permission_exists.sql.erb @@ -0,0 +1,8 @@ +USE [master]; +DECLARE @perm_state varchar(250); +SET @perm_state = <%= scope.function_template(['sqlserver/snippets/login/get_perm_state.sql.erb']) %>; +DECLARE @error_msg varchar(250); +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/login/get_perm_state.sql.erb b/templates/snippets/login/get_perm_state.sql.erb new file mode 100644 index 00000000..e1d9dd84 --- /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') From 12de69b92638d93cf2539c615256184247029061 Mon Sep 17 00:00:00 2001 From: Travis Fields Date: Tue, 24 Feb 2015 17:59:02 -0800 Subject: [PATCH 2/2] FM-1556 Change permission to permissions and allow for array --- manifests/login/permission.pp | 17 +++++----- spec/defines/login/permission_spec.rb | 34 +++++++++---------- templates/create/login/permission.sql.erb | 14 +++++--- .../query/login/permission_exists.sql.erb | 14 ++++---- .../snippets/login/get_perm_state.sql.erb | 2 +- .../snippets/login/permission/exists.sql.erb | 4 +++ 6 files changed, 48 insertions(+), 37 deletions(-) create mode 100644 templates/snippets/login/permission/exists.sql.erb diff --git a/manifests/login/permission.pp b/manifests/login/permission.pp index c66a66b0..81be1e17 100644 --- a/manifests/login/permission.pp +++ b/manifests/login/permission.pp @@ -10,8 +10,8 @@ # [login] # The login for which the permission will be manage. # -# [permission] -# The permission you would like managed. i.e. 'SELECT', 'INSERT', 'UPDATE', 'DELETE' +# [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. @@ -22,7 +22,7 @@ ## define sqlserver::login::permission ( $login, - $permission = $title, + $permissions, $state = 'GRANT', $with_grant_option = false, $instance = 'MSSQLSERVER', @@ -30,9 +30,8 @@ sqlserver_validate_instance_name($instance) ## Validate Permissions - $_permission = upcase($permission) - sqlserver_validate_range($_permission, 4, 128, 'Permission must be between 4 and 128 characters') - validate_re($_permission, '^([A-Z]|\s)+$', 'Permissions must be alphabetic only') + 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') @@ -41,8 +40,10 @@ 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) - - sqlserver_tsql{ "login-permission-${instance}-${login}-${_permission}": + 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'), diff --git a/spec/defines/login/permission_spec.rb b/spec/defines/login/permission_spec.rb index bfc09bb1..f34c77a9 100644 --- a/spec/defines/login/permission_spec.rb +++ b/spec/defines/login/permission_spec.rb @@ -6,11 +6,11 @@ context 'validation errors' do include_context 'manifests' do let(:title) { 'myTitle' } - let(:sqlserver_tsql_title) { 'login-permission-MSSQLSERVER-loggingUser-SELECT' } + let(:sqlserver_tsql_title) { 'login-permission-MSSQLSERVER-loggingUser-GRANT' } end context 'login =>' do let(:params) { { - :permission => 'SELECT', + :permissions=> ['SELECT'], } } let(:raise_error_check) { 'Login must be between 1 and 128 characters' } describe 'missing' do @@ -32,21 +32,21 @@ } } let(:raise_error_check) { 'Permission must be between 4 and 128 characters' } describe 'empty' do - let(:additional_params) { {:permission => ''} } + let(:additional_params) { {:permissions=> ['']} } it_behaves_like 'validation error' end describe 'under limit' do - let(:additional_params) { {:permission => random_string_of_size(3, false)} } + let(:additional_params) { {:permissions=> [random_string_of_size(3, false)]} } it_behaves_like 'validation error' end describe 'over limit' do - let(:additional_params) { {:permission => random_string_of_size(129, false)} } + let(:additional_params) { {:permissions=> [random_string_of_size(129, false)]} } it_behaves_like 'validation error' end end context 'state =>' do let(:params) { { - :permission => 'SELECT', + :permissions=> ['SELECT'], :login => 'loggingUser' } } describe 'invalid' do @@ -59,15 +59,15 @@ context 'successfully' do include_context 'manifests' do let(:title) { 'myTitle' } - let(:sqlserver_tsql_title) { 'login-permission-MSSQLSERVER-loggingUser-SELECT' } + let(:sqlserver_tsql_title) { 'login-permission-MSSQLSERVER-loggingUser-GRANT' } let(:params) { { :login => 'loggingUser', - :permission => 'SELECT', + :permissions=> ['SELECT'], } } end %w(revoke grant deny).each do |state| context "state => '#{state}'" do - let(:sqlserver_tsql_title) { "login-permission-MSSQLSERVER-loggingUser-SELECT" } + 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} } @@ -84,15 +84,15 @@ context 'permission' do describe 'upper limit' do permission =random_string_of_size(128, false) - let(:additional_params) { {:permission => permission} } - let(:sqlserver_tsql_title) { "login-permission-MSSQLSERVER-loggingUser-#{permission.upcase}" } + 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) { {:permission => 'ALTER'} } + let(:additional_params) { {:permissions=> ['ALTER']} } let(:should_contain_command) { ['USE [master];', 'GRANT ALTER TO [loggingUser];'] } - let(:sqlserver_tsql_title) { "login-permission-MSSQLSERVER-loggingUser-ALTER" } + let(:sqlserver_tsql_title) { "login-permission-MSSQLSERVER-loggingUser-GRANT" } it_behaves_like 'sqlserver_tsql command' end end @@ -111,10 +111,10 @@ context 'command syntax' do include_context 'manifests' do let(:title) { 'myTitle' } - let(:sqlserver_tsql_title) { 'login-permission-MSSQLSERVER-loggingUser-SELECT' } + let(:sqlserver_tsql_title) { 'login-permission-MSSQLSERVER-loggingUser-GRANT' } let(:params) { { :login => 'loggingUser', - :permission => 'SELECT', + :permissions => ['SELECT'], } } describe '' do let(:should_contain_command) { [ @@ -123,8 +123,8 @@ /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 = 'SELECT'\),\n\s+'REVOKE'\)/, - /DECLARE @error_msg varchar\(250\);\nSET @error_msg = 'EXPECTED login \[loggingUser\] to have permission \[SELECT\] with GRANT but got ' \+ @perm_state;/, + /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' diff --git a/templates/create/login/permission.sql.erb b/templates/create/login/permission.sql.erb index d17c34e4..eb4c97f2 100644 --- a/templates/create/login/permission.sql.erb +++ b/templates/create/login/permission.sql.erb @@ -1,11 +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 - USE [master]; <% 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; + REVOKE GRANT OPTION FOR <%= permission %> TO [<%= @login %>] CASCADE; <% end %> - <%= @_state %> <%= @_permission %> TO [<%= @login %>]<% if @with_grant_option == true %> WITH GRANT OPTION<% end %>; + <%= @_state %> <%= permission %> TO [<%= @login %>]<% if @with_grant_option == true %> WITH GRANT OPTION<% end %>; END BEGIN - <%= scope.function_template(['sqlserver/query/login/permission_exists.sql.erb']) %> + <%= scope.function_template(['sqlserver/snippets/login/permission/exists.sql.erb']) %> END +<% end %> diff --git a/templates/query/login/permission_exists.sql.erb b/templates/query/login/permission_exists.sql.erb index 7f333128..d6023cda 100644 --- a/templates/query/login/permission_exists.sql.erb +++ b/templates/query/login/permission_exists.sql.erb @@ -1,8 +1,8 @@ USE [master]; -DECLARE @perm_state varchar(250); -SET @perm_state = <%= scope.function_template(['sqlserver/snippets/login/get_perm_state.sql.erb']) %>; -DECLARE @error_msg varchar(250); -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 +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/snippets/login/get_perm_state.sql.erb b/templates/snippets/login/get_perm_state.sql.erb index e1d9dd84..82b3b4a3 100644 --- a/templates/snippets/login/get_perm_state.sql.erb +++ b/templates/snippets/login/get_perm_state.sql.erb @@ -3,5 +3,5 @@ ISNULL( 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 %>'), + 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;