summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Smith <tsmith@chef.io>2020-02-26 13:02:34 -0800
committerGitHub <noreply@github.com>2020-02-26 13:02:34 -0800
commitdf927bec7d28bb72f0a7e65cbafa6455b280a4c3 (patch)
tree87c1597848312a92c502e273feb5993c8b848572
parent4703a934ddfd4af48ecd3dbb1e6253fd5b74f179 (diff)
parent11a63c5b61ca1d703b368ddc1caecd7f16fc7e9c (diff)
downloadchef-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.rb157
-rw-r--r--lib/chef/resources.rb1
-rw-r--r--lib/chef/win32/api/security.rb6
-rw-r--r--lib/chef/win32/security.rb42
-rw-r--r--spec/functional/resource/windows_user_privilege_spec.rb193
-rw-r--r--spec/functional/win32/security_spec.rb22
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.