diff options
author | Tim Smith <tsmith@chef.io> | 2020-02-26 13:02:34 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-02-26 13:02:34 -0800 |
commit | df927bec7d28bb72f0a7e65cbafa6455b280a4c3 (patch) | |
tree | 87c1597848312a92c502e273feb5993c8b848572 | |
parent | 4703a934ddfd4af48ecd3dbb1e6253fd5b74f179 (diff) | |
parent | 11a63c5b61ca1d703b368ddc1caecd7f16fc7e9c (diff) | |
download | chef-df927bec7d28bb72f0a7e65cbafa6455b280a4c3.tar.gz |
Merge pull request #9279 from MsysTechnologiesllc/vasundhara/MSYS-1230_Windows_user_privilege_resource
Migrating windows_user_privilege resource from windows cookbook
-rw-r--r-- | lib/chef/resource/windows_user_privilege.rb | 157 | ||||
-rw-r--r-- | lib/chef/resources.rb | 1 | ||||
-rw-r--r-- | lib/chef/win32/api/security.rb | 6 | ||||
-rw-r--r-- | lib/chef/win32/security.rb | 42 | ||||
-rw-r--r-- | spec/functional/resource/windows_user_privilege_spec.rb | 193 | ||||
-rw-r--r-- | spec/functional/win32/security_spec.rb | 22 |
6 files changed, 419 insertions, 2 deletions
diff --git a/lib/chef/resource/windows_user_privilege.rb b/lib/chef/resource/windows_user_privilege.rb new file mode 100644 index 0000000000..685354cfb4 --- /dev/null +++ b/lib/chef/resource/windows_user_privilege.rb @@ -0,0 +1,157 @@ +# +# Author:: Jared Kauppila (<jared@kauppi.la>) +# Author:: Vasundhara Jagdale(<vasundhara.jagdale@chef.io>) +# Copyright 2008-2020, Chef Software, Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require_relative "../resource" + +class Chef + class Resource + class WindowsUserPrivilege < Chef::Resource + privilege_opts = %w{SeTrustedCredManAccessPrivilege + SeNetworkLogonRight + SeTcbPrivilege + SeMachineAccountPrivilege + SeIncreaseQuotaPrivilege + SeInteractiveLogonRight + SeRemoteInteractiveLogonRight + SeBackupPrivilege + SeChangeNotifyPrivilege + SeSystemtimePrivilege + SeTimeZonePrivilege + SeCreatePagefilePrivilege + SeCreateTokenPrivilege + SeCreateGlobalPrivilege + SeCreatePermanentPrivilege + SeCreateSymbolicLinkPrivilege + SeDebugPrivilege + SeDenyNetworkLogonRight + SeDenyBatchLogonRight + SeDenyServiceLogonRight + SeDenyInteractiveLogonRight + SeDenyRemoteInteractiveLogonRight + SeEnableDelegationPrivilege + SeRemoteShutdownPrivilege + SeAuditPrivilege + SeImpersonatePrivilege + SeIncreaseWorkingSetPrivilege + SeIncreaseBasePriorityPrivilege + SeLoadDriverPrivilege + SeLockMemoryPrivilege + SeBatchLogonRight + SeServiceLogonRight + SeSecurityPrivilege + SeRelabelPrivilege + SeSystemEnvironmentPrivilege + SeManageVolumePrivilege + SeProfileSingleProcessPrivilege + SeSystemProfilePrivilege + SeUndockPrivilege + SeAssignPrimaryTokenPrivilege + SeRestorePrivilege + SeShutdownPrivilege + SeSyncAgentPrivilege + SeTakeOwnershipPrivilege + } + + resource_name :windows_user_privilege + description "The windows_user_privilege resource allows to add and set principal (User/Group) to the specified privilege. \n Ref: https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/user-rights-assignment" + + introduced "16.0" + + property :principal, String, + description: "An optional property to add the user to the given privilege. Use only with add and remove action.", + name_property: true + + property :users, Array, + description: "An optional property to set the privilege for given users. Use only with set action." + + property :privilege, [Array, String], + description: "Privilege to set for users.", + required: true, + coerce: proc { |v| v.is_a?(String) ? Array[v] : v }, + callbacks: { + "Option privilege must include any of the: #{privilege_opts}" => lambda { + |v| (privilege_opts & v).size == v.size + }, + } + + load_current_value do |new_resource| + unless new_resource.principal.nil? + privilege Chef::ReservedNames::Win32::Security.get_account_right(new_resource.principal) unless new_resource.action.include?(:set) + end + end + + action :add do + ([*new_resource.privilege] - [*current_resource.privilege]).each do |user_right| + converge_by("adding user '#{new_resource.principal}' privilege #{user_right}") do + Chef::ReservedNames::Win32::Security.add_account_right(new_resource.principal, user_right) + end + end + end + + action :set do + if new_resource.users.nil? || new_resource.users.empty? + raise Chef::Exceptions::ValidationFailed, "Users are required property with set action." + end + + users = [] + + # Getting users with its domain for comparison + new_resource.users.each do |user| + user = Chef::ReservedNames::Win32::Security.lookup_account_name(user) + users << user[1].account_name if user + end + + new_resource.privilege.each do |privilege| + accounts = Chef::ReservedNames::Win32::Security.get_account_with_user_rights(privilege) + + # comparing the existing accounts for privilege with users + unless users == accounts + # Removing only accounts which is not matching with users in new_resource + (accounts - users).each do |account| + converge_by("removing user '#{account}' from privilege #{privilege}") do + Chef::ReservedNames::Win32::Security.remove_account_right(account, privilege) + end + end + + # Adding only users which is not already exist + (users - accounts).each do |user| + converge_by("adding user '#{user}' to privilege #{privilege}") do + Chef::ReservedNames::Win32::Security.add_account_right(user, privilege) + end + end + end + end + end + + action :remove do + curr_res_privilege = current_resource.privilege + missing_res_privileges = (new_resource.privilege - curr_res_privilege) + + if missing_res_privileges + Chef::Log.info("User \'#{new_resource.principal}\' for Privilege: #{missing_res_privileges.join(", ")} not found. Nothing to remove.") + end + + (new_resource.privilege - missing_res_privileges).each do |user_right| + converge_by("removing user #{new_resource.principal} from privilege #{user_right}") do + Chef::ReservedNames::Win32::Security.remove_account_right(new_resource.principal, user_right) + end + end + end + end + end +end diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb index 3681d9bd54..8bdd207e84 100644 --- a/lib/chef/resources.rb +++ b/lib/chef/resources.rb @@ -158,3 +158,4 @@ require_relative "resource/windows_task" require_relative "resource/windows_uac" require_relative "resource/windows_workgroup" require_relative "resource/timezone" +require_relative "resource/windows_user_privilege" diff --git a/lib/chef/win32/api/security.rb b/lib/chef/win32/api/security.rb index b651283758..16671a9f6d 100644 --- a/lib/chef/win32/api/security.rb +++ b/lib/chef/win32/api/security.rb @@ -413,6 +413,11 @@ class Chef :Buffer, :PWSTR end + # https://docs.microsoft.com/en-us/windows/win32/api/ntsecapi/ns-ntsecapi-lsa_enumeration_information + class LSA_ENUMERATION_INFORMATION < FFI::Struct + layout :Sid, :PSID + end + ffi_lib "advapi32" safe_attach_function :AccessCheck, %i{pointer HANDLE DWORD pointer pointer pointer pointer pointer}, :BOOL @@ -448,6 +453,7 @@ class Chef safe_attach_function :LookupPrivilegeDisplayNameW, %i{LPCWSTR LPCWSTR LPWSTR LPDWORD LPDWORD}, :BOOL safe_attach_function :LookupPrivilegeValueW, %i{LPCWSTR LPCWSTR PLUID}, :BOOL safe_attach_function :LsaAddAccountRights, %i{pointer pointer pointer ULONG}, :NTSTATUS + safe_attach_function :LsaEnumerateAccountsWithUserRight, %i{LSA_HANDLE PLSA_UNICODE_STRING PVOID PULONG}, :NTSTATUS safe_attach_function :LsaRemoveAccountRights, %i{pointer pointer BOOL pointer ULONG}, :NTSTATUS safe_attach_function :LsaClose, [ :LSA_HANDLE ], :NTSTATUS safe_attach_function :LsaEnumerateAccountRights, %i{LSA_HANDLE PSID PLSA_UNICODE_STRING PULONG}, :NTSTATUS diff --git a/lib/chef/win32/security.rb b/lib/chef/win32/security.rb index 5b78b652eb..2c0f63684a 100644 --- a/lib/chef/win32/security.rb +++ b/lib/chef/win32/security.rb @@ -214,6 +214,41 @@ class Chef privileges end + def self.get_account_with_user_rights(privilege) + privilege_pointer = FFI::MemoryPointer.new LSA_UNICODE_STRING, 1 + privilege_lsa_string = LSA_UNICODE_STRING.new(privilege_pointer) + privilege_lsa_string[:Buffer] = FFI::MemoryPointer.from_string(privilege.to_wstring) + privilege_lsa_string[:Length] = privilege.length * 2 + privilege_lsa_string[:MaximumLength] = (privilege.length + 1) * 2 + + buffer = FFI::MemoryPointer.new(:pointer) + count = FFI::MemoryPointer.new(:ulong) + + accounts = [] + with_lsa_policy(nil) do |policy_handle, sid| + result = LsaEnumerateAccountsWithUserRight(policy_handle.read_pointer, privilege_pointer, buffer, count) + if result == 0 + win32_error = LsaNtStatusToWinError(result) + return [] if win32_error == 1313 # NO_SUCH_PRIVILEGE - https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--1300-1699- + + test_and_raise_lsa_nt_status(result) + + count.read_ulong.times do |i| + sid = LSA_ENUMERATION_INFORMATION.new(buffer.read_pointer + i * LSA_ENUMERATION_INFORMATION.size) + sid_name = lookup_account_sid(sid[:Sid]) + domain, name, use = sid_name + account_name = (!domain.nil? && domain.length > 0) ? "#{domain}\\#{name}" : name + accounts << account_name + end + end + + result = LsaFreeMemory(buffer.read_pointer) + test_and_raise_lsa_nt_status(result) + end + + accounts + end + def self.get_ace(acl, index) acl = acl.pointer if acl.respond_to?(:pointer) ace = FFI::Buffer.new :pointer @@ -616,18 +651,21 @@ class Chef end def self.with_lsa_policy(username) - sid = lookup_account_name(username)[1] + sid = lookup_account_name(username)[1] if username access = 0 access |= POLICY_CREATE_ACCOUNT access |= POLICY_LOOKUP_NAMES + access |= POLICY_VIEW_LOCAL_INFORMATION if username.nil? policy_handle = FFI::MemoryPointer.new(:pointer) result = LsaOpenPolicy(nil, LSA_OBJECT_ATTRIBUTES.new, access, policy_handle) test_and_raise_lsa_nt_status(result) + sid_pointer = username.nil? ? nil : sid.pointer + begin - yield policy_handle, sid.pointer + yield policy_handle, sid_pointer ensure result = LsaClose(policy_handle.read_pointer) test_and_raise_lsa_nt_status(result) diff --git a/spec/functional/resource/windows_user_privilege_spec.rb b/spec/functional/resource/windows_user_privilege_spec.rb new file mode 100644 index 0000000000..6dca54016a --- /dev/null +++ b/spec/functional/resource/windows_user_privilege_spec.rb @@ -0,0 +1,193 @@ +# +# Author:: Vasundhara Jagdale (<vasundhara.jagdale@chef.io>) +# Copyright 2008-2020, Chef Software, Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require_relative "../../spec_helper" +require_relative "../../functional/resource/base" + +describe Chef::Resource::WindowsUserPrivilege, :windows_only do + let(:principal) { nil } + let(:privilege) { nil } + let(:users) { nil } + let(:sensitive) { true } + + let(:windows_test_run_context) do + node = Chef::Node.new + node.consume_external_attrs(OHAI_SYSTEM.data, {}) # node[:languages][:powershell][:version] + node.automatic["os"] = "windows" + node.automatic["platform"] = "windows" + node.automatic["platform_version"] = "6.1" + node.automatic["kernel"][:machine] = :x86_64 # Only 64-bit architecture is supported + empty_events = Chef::EventDispatch::Dispatcher.new + Chef::RunContext.new(node, {}, empty_events) + end + + subject do + new_resource = Chef::Resource::WindowsUserPrivilege.new(principal, windows_test_run_context) + new_resource.privilege = privilege + new_resource.principal = principal + new_resource.users = users + new_resource + end + + describe "#add privilege" do + after { subject.run_action(:remove) } + + context "when privilege is passed as string" do + let(:principal) { "Administrator" } + let(:privilege) { "SeCreateSymbolicLinkPrivilege" } + + it "adds user to privilege" do + # Removing so that add update happens + subject.run_action(:remove) + subject.run_action(:add) + expect(subject).to be_updated_by_last_action + end + + it "is idempotent" do + subject.run_action(:add) + subject.run_action(:add) + expect(subject).not_to be_updated_by_last_action + end + end + + context "when privilege is passed as array" do + let(:principal) { "Administrator" } + let(:privilege) { %w{SeCreateSymbolicLinkPrivilege SeCreatePagefilePrivilege} } + + it "adds user to privilege" do + subject.run_action(:add) + expect(subject).to be_updated_by_last_action + end + + it "is idempotent" do + subject.run_action(:add) + subject.run_action(:add) + expect(subject).not_to be_updated_by_last_action + end + end + end + + describe "#set privilege" do + after { remove_user_privilege("Administrator", subject.privilege) } + + let(:principal) { "user_privilege" } + let(:users) { %w{Administrators Administrator} } + let(:privilege) { %w{SeCreateSymbolicLinkPrivilege} } + + it "sets user to privilege" do + subject.action(:set) + subject.run_action(:set) + expect(subject).to be_updated_by_last_action + end + + it "is idempotent" do + subject.action(:set) + subject.run_action(:set) + subject.run_action(:set) + expect(subject).not_to be_updated_by_last_action + end + + it "raise error if users not provided" do + subject.users = nil + subject.action(:set) + expect { subject.run_action(:set) }.to raise_error(Chef::Exceptions::ValidationFailed) + end + end + + describe "#remove privilege" do + let(:principal) { "Administrator" } + context "when privilege is passed as array" do + let(:privilege) { "SeCreateSymbolicLinkPrivilege" } + it "remove user from privilege" do + subject.run_action(:add) + subject.run_action(:remove) + expect(subject).to be_updated_by_last_action + end + + it "is idempotent" do + subject.run_action(:add) + subject.run_action(:remove) + subject.run_action(:remove) + expect(subject).not_to be_updated_by_last_action + end + end + + context "when privilege is passed as array" do + let(:privilege) { %w{SeCreateSymbolicLinkPrivilege SeCreatePagefilePrivilege} } + it "remove user from privilege" do + subject.run_action(:add) + subject.run_action(:remove) + expect(subject).to be_updated_by_last_action + end + + it "is idempotent" do + subject.run_action(:add) + subject.run_action(:remove) + subject.run_action(:remove) + expect(subject).not_to be_updated_by_last_action + end + end + end + + describe "running with non admin user" do + include Chef::Mixin::UserContext + + let(:user) { "security_user" } + let(:password) { "Security@123" } + let(:principal) { "user_privilege" } + let(:users) { ["Administrators", "#{domain}\\security_user"] } + let(:privilege) { %w{SeCreateSymbolicLinkPrivilege} } + + let(:domain) do + ENV["COMPUTERNAME"] + end + + before do + allow_any_instance_of(Chef::Mixin::UserContext).to receive(:node).and_return({ "platform_family" => "windows" }) + add_user = Mixlib::ShellOut.new("net user #{user} #{password} /ADD") + add_user.run_command + add_user.error! + end + + after do + remove_user_privilege("#{domain}\\#{user}", subject.privilege) + delete_user = Mixlib::ShellOut.new("net user #{user} /delete") + delete_user.run_command + delete_user.error! + end + + it "sets user to privilege" do + subject.action(:set) + subject.run_action(:set) + expect(subject).to be_updated_by_last_action + end + + it "is idempotent" do + subject.action(:set) + subject.run_action(:set) + subject.run_action(:set) + expect(subject).not_to be_updated_by_last_action + end + end + + def remove_user_privilege(user, privilege) + subject.action(:remove) + subject.principal = user + subject.privilege = privilege + subject.run_action(:remove) + end +end diff --git a/spec/functional/win32/security_spec.rb b/spec/functional/win32/security_spec.rb index 3eb7bedd48..c01e9be9a3 100644 --- a/spec/functional/win32/security_spec.rb +++ b/spec/functional/win32/security_spec.rb @@ -199,6 +199,28 @@ describe "Chef::Win32::Security", :windows_only do end end + describe ".get_account_with_user_rights" do + let(:domain) { ENV["COMPUTERNAME"] } + let(:username) { ENV["USERNAME"] } + + context "when given a valid user right" do + it "gets all accounts associated with given user right" do + Chef::ReservedNames::Win32::Security.add_account_right(username, "SeBatchLogonRight") + expect(Chef::ReservedNames::Win32::Security.get_account_with_user_rights("SeBatchLogonRight").flatten).to include("#{domain}\\#{username}") + Chef::ReservedNames::Win32::Security.remove_account_right(username, "SeBatchLogonRight") + expect(Chef::ReservedNames::Win32::Security.get_account_with_user_rights("SeBatchLogonRight").flatten).not_to include("#{domain}\\#{username}") + end + end + + context "when given an invalid user right" do + let(:user_right) { "SeTest" } + + it "returns empty array" do + expect(Chef::ReservedNames::Win32::Security.get_account_with_user_rights(user_right)).to be_empty + end + end + end + describe ".test_and_raise_lsa_nt_status" do # NTSTATUS code: 0xC0000001 / STATUS_UNSUCCESSFUL # Windows Error: ERROR_GEN_FAILURE / 31 / 0x1F / A device attached to the system is not functioning. |