Extend Windows ACL matchers (#1744)

* Adds alias for 'ListDirectory' permission

Signed-off-by: David Alexander <opensource@thelonelyghost.com>

* Works with Ruby array of permissions as long as possible

Converts to PowerShell array just before use.

Signed-off-by: David Alexander <opensource@thelonelyghost.com>

* Abstracts user-provided permissions to router method

Signed-off-by: David Alexander <opensource@thelonelyghost.com>

* Adds FullControl as a specifiable permission

Signed-off-by: David Alexander <opensource@thelonelyghost.com>

* Adds specific permission 'modify'

Signed-off-by: David Alexander <opensource@thelonelyghost.com>

* Fixes #1743

Limits Windows' broad "read" permission to if it can read all of the
above, instead of just the first:

- File contents
- File attributes
- File extended attributes
- File permissions

This better aligns with how Windows names the permissions.

  'read' -> Read instead of 'read' -> ReadData

Signed-off-by: David Alexander <opensource@thelonelyghost.com>

* 'Execute' Windows ACL has alias of 'Traverse'

Signed-off-by: David Alexander <opensource@thelonelyghost.com>

* Adds 'Delete' permission

Signed-off-by: David Alexander <opensource@thelonelyghost.com>

* Adds `should allow('perm').by_user('me')` matcher

Provides hooks for later use with Windows ACL matching

Signed-off-by: David Alexander <opensource@thelonelyghost.com>

* Adds remaining Windows ACL hooks

Skips ReadAndExecute on intentionally since it just aliases the combo of
2 permissions into one new one.

Signed-off-by: David Alexander <opensource@thelonelyghost.com>

* [Rubocop] Reduces ABC / Cyclomatic complexity

Signed-off-by: David Alexander <opensource@thelonelyghost.com>

* Reduces global scope with `allows()` -> `be_allowed()`

RSpec inferred matchers work nicely here. This changes the `by_user()`
and `by()` chained matchers to just be an options hash on the underlying
`allowed?()` method.

Signed-off-by: David Alexander <opensource@thelonelyghost.com>

* Fixes integration tests with rename `allows()` -> `be_allowed()`

Signed-off-by: David Alexander <opensource@thelonelyghost.com>
This commit is contained in:
David Alexander 2017-10-17 09:01:51 -04:00 committed by Christoph Hartmann
parent e5ce31fcc7
commit 6ed4068fd1
3 changed files with 129 additions and 10 deletions

View file

@ -84,6 +84,13 @@ module Inspec::Resources
file_permission_granted?('execute', by_usergroup, by_specific_user)
end
def allowed?(permission, opts = {})
return false unless exist?
return skip_resource '`allowed?` is not supported on your OS yet.' if @perms_provider.nil?
file_permission_granted?(permission, opts[:by], opts[:by_user])
end
def mounted?(expected_options = nil, identical = false)
mounted = file.mounted
@ -206,18 +213,82 @@ module Inspec::Resources
end
def check_file_permission_by_user(access_type, user, path)
access_rule = case access_type
when 'read'
'@(\'FullControl\', \'Modify\', \'ReadAndExecute\', \'Read\', \'ListDirectory\')'
when 'write'
'@(\'FullControl\', \'Modify\', \'Write\')'
when 'execute'
'@(\'FullControl\', \'Modify\', \'ReadAndExecute\', \'ExecuteFile\')'
else
raise 'Invalid access_type provided'
end
access_rule = translate_perm_names(access_type)
access_rule = convert_to_powershell_array(access_rule)
cmd = inspec.command("@(@((Get-Acl '#{path}').access | Where-Object {$_.AccessControlType -eq 'Allow' -and $_.IdentityReference -eq '#{user}' }) | Where-Object {($_.FileSystemRights.ToString().Split(',') | % {$_.trim()} | ? {#{access_rule} -contains $_}) -ne $null}) | measure | % { $_.Count }")
cmd.stdout.chomp == '0' ? false : true
end
private
def convert_to_powershell_array(arr)
if arr.empty?
'@()'
else
%{@('#{arr.join("', '")}')}
end
end
# Translates a developer-friendly string into a list of acceptable
# FileSystemRights that match it, because Windows has a fun heirarchy
# of permissions that are able to be noted in multiple ways.
#
# See also: https://www.codeproject.com/Reference/871338/AccessControl-FileSystemRights-Permissions-Table
def translate_perm_names(access_type)
names = translate_common_perms(access_type)
names ||= translate_granular_perms(access_type)
names ||= translate_uncommon_perms(access_type)
raise 'Invalid access_type provided' unless names
end
def translate_common_perms(access_type)
case access_type
when 'full-control'
%w{FullControl}
when 'modify'
translate_perm_names('full-control') + %w{Modify}
when 'read'
translate_perm_names('modify') + %w{ReadAndExecute Read}
when 'write'
translate_perm_names('modify') + %w{Write}
when 'execute'
translate_perm_names('modify') + %w{ReadAndExecute ExecuteFile Traverse}
when 'delete'
translate_perm_names('modify') + %w{Delete}
end
end
def translate_uncommon_perms(access_type)
case access_type
when 'delete-subdirectories-and-files'
translate_perm_names('full-control') + %w{DeleteSubdirectoriesAndFiles}
when 'change-permissions'
translate_perm_names('full-control') + %w{ChangePermissions}
when 'take-ownership'
translate_perm_names('full-control') + %w{TakeOwnership}
end
end
def translate_granular_perms(access_type)
case access_type
when 'write-data', 'create-files'
translate_perm_names('write') + %w{WriteData CreateFiles}
when 'append-data', 'create-directories'
translate_perm_names('write') + %w{CreateDirectories AppendData}
when 'write-extended-attributes'
translate_perm_names('write') + %w{WriteExtendedAttributes}
when 'write-attributes'
translate_perm_names('write') + %w{WriteAttributes}
when 'read-data', 'list-directory'
translate_perm_names('read') + %w{ReadData ListDirectory}
when 'read-attributes'
translate_perm_names('read') + %w{ReadAttributes}
when 'read-extended-attributes'
translate_perm_names('read') + %w{ReadExtendedAttributes}
when 'read-permissions'
translate_perm_names('read') + %w{ReadPermissions}
end
end
end
end

View file

@ -67,31 +67,50 @@ if os.unix?
its('sticky') { should eq false }
it { should be_readable }
it { should be_allowed('read') }
it { should be_readable.by('owner') }
it { should be_allowed('read', by: 'owner') }
it { should be_readable.by('group') }
it { should be_allowed('read', by: 'group') }
it { should be_readable.by('other') }
it { should be_allowed('read', by: 'other') }
it { should be_readable.by_user(filedata[:user]) }
it { should be_allowed('read', by_user: filedata[:user]) }
it { should_not be_readable.by_user('noroot') }
it { should_not be_allowed('read', by_user: 'noroot') }
# for server spec compatibility
it { should be_readable.by('others') }
it { should be_allowed('read', by: 'others') }
it { should be_writable }
it { should be_allowed('write') }
it { should be_writable.by('owner') }
it { should be_allowed('write', by: 'owner') }
it { should be_writable.by('group') }
it { should be_allowed('write', by: 'group') }
it { should_not be_writable.by('other') }
it { should_not be_allowed('write', by: 'other') }
it { should be_writable.by_user(filedata[:user]) }
it { should be_allowed('write', by_user: filedata[:user]) }
# it { should_not be_writable.by_user('noroot') }
# for server spec compatibility
it { should_not be_writable.by('others') }
it { should_not be_allowed('write', by: 'others') }
it { should be_executable }
it { should be_allowed('execute') }
it { should be_executable.by('owner') }
it { should be_allowed('execute', by: 'owner') }
it { should_not be_executable.by('group') }
it { should_not be_allowed('execute', by: 'group') }
it { should be_executable.by('other') }
it { should be_allowed('execute', by: 'other') }
it { should be_executable.by_user(filedata[:user]) }
it { should be_allowed('execute', by_user: filedata[:user]) }
# it { should_not be_executable.by_user('noroot') }
# for server spec compatibility
it { should be_executable.by('others') }
it { should be_allowed('execute', by: 'others') }
# test extended linux attributes
# it { should be_immutable }
@ -167,22 +186,34 @@ if os.windows?
it { should exist }
it { should be_file }
it { should be_readable.by_user('NT AUTHORITY\SYSTEM') }
it { should be_allowed('read', by_user: 'NT AUTHORITY\SYSTEM') }
it { should be_writable.by_user('NT AUTHORITY\SYSTEM') }
it { should be_allowed('write', by_user: 'NT AUTHORITY\SYSTEM') }
it { should be_executable.by_user('NT AUTHORITY\SYSTEM') }
it { should be_allowed('execute', by_user: 'NT AUTHORITY\SYSTEM') }
it { should_not be_readable.by_user(filedata[:user]) }
it { should_not be_allowed('read', by_user: filedata[:user]) }
it { should_not be_writable.by_user(filedata[:user]) }
it { should_not be_allowed('write', by_user: filedata[:user]) }
it { should_not be_executable.by_user(filedata[:user]) }
it { should_not be_allowed('execute', by_user: filedata[:user]) }
end
describe file('C:/Test Directory') do
it { should exist }
it { should be_directory }
it { should be_readable.by_user('NT AUTHORITY\SYSTEM') }
it { should be_allowed('read', by_user: 'NT AUTHORITY\SYSTEM') }
it { should be_writable.by_user('NT AUTHORITY\SYSTEM') }
it { should be_allowed('write', by_user: 'NT AUTHORITY\SYSTEM') }
it { should be_executable.by_user('NT AUTHORITY\SYSTEM') }
it { should be_allowed('execute', by_user: 'NT AUTHORITY\SYSTEM') }
it { should_not be_readable.by_user(filedata[:user]) }
it { should_not be_allowed('read', by_user: filedata[:user]) }
it { should_not be_writable.by_user(filedata[:user]) }
it { should_not be_allowed('write', by_user: filedata[:user]) }
it { should_not be_executable.by_user(filedata[:user]) }
it { should_not be_allowed('execute', by_user: filedata[:user]) }
end
describe file("C:/Program Files (x86)/Windows NT/Accessories/wordpad.exe") do
@ -195,5 +226,7 @@ if os.windows?
describe directory('C:/opscode/chef') do
its('owner') { should cmp 'NT AUTHORITY\SYSTEM' }
it { should be_owned_by 'NT AUTHORITY\SYSTEM' }
it { should be_allowed('full-control', by_user: 'NT AUTHORITY\SYSTEM') }
it { should be_allowed('modify', by_user: 'NT AUTHORITY\SYSTEM') }
end
end

View file

@ -26,8 +26,11 @@ describe Inspec::Resources::FileResource do
_(resource.mounted?).must_equal true
_(resource.to_s).must_equal 'File /fakepath/fakefile'
_(resource.readable?('by_usergroup', 'by_specific_user')).must_equal 'test_result'
_(resource.allowed?('read', by: 'by_usergroup', by_user: 'by_specific_user')).must_equal 'test_result'
_(resource.writable?('by_usergroup', 'by_specific_user')).must_equal 'test_result'
_(resource.allowed?('write', by: 'by_usergroup', by_user: 'by_specific_user')).must_equal 'test_result'
_(resource.executable?('by_usergroup', 'by_specific_user')).must_equal 'test_result'
_(resource.allowed?('execute', by: 'by_usergroup', by_user: 'by_specific_user')).must_equal 'test_result'
_(resource.suid).must_equal true
_(resource.sgid).must_equal true
_(resource.sticky).must_equal true
@ -40,12 +43,23 @@ describe Inspec::Resources::FileResource do
resource.stubs(:file_permission_granted?).with('read', 'by_usergroup', 'by_specific_user').returns('test_result')
resource.stubs(:file_permission_granted?).with('write', 'by_usergroup', 'by_specific_user').returns('test_result')
resource.stubs(:file_permission_granted?).with('execute', 'by_usergroup', 'by_specific_user').returns('test_result')
resource.stubs(:file_permission_granted?).with('full-control', 'by_usergroup', 'by_specific_user').returns('test_result')
_(resource.content).must_equal 'content'
_(resource.exist?).must_equal true
_(resource.mounted?).must_equal true
_(resource.readable?('by_usergroup', 'by_specific_user')).must_equal 'test_result'
_(resource.allowed?('read', by: 'by_usergroup', by_user: 'by_specific_user')).must_equal 'test_result'
_(resource.writable?('by_usergroup', 'by_specific_user')).must_equal 'test_result'
_(resource.allowed?('write', by: 'by_usergroup', by_user: 'by_specific_user')).must_equal 'test_result'
_(resource.executable?('by_usergroup', 'by_specific_user')).must_equal 'test_result'
_(resource.allowed?('execute', by: 'by_usergroup', by_user: 'by_specific_user')).must_equal 'test_result'
_(resource.allowed?('full-control', by: 'by_usergroup', by_user: 'by_specific_user')).must_equal 'test_result'
end
it 'does not support Windows-style ACL on Ubuntu' do
resource = MockLoader.new(:ubuntu1404).load_resource('file', '/fakepath/fakefile')
resource.stubs(:exist?).returns(true)
proc { resource.send('allowed?', 'full-control', { by: 'by_usergroup', by_user: 'by_specific_user' }) }.must_raise(RuntimeError)
proc { resource.send('allowed?', 'modify', { by: 'by_usergroup', by_user: 'by_specific_user' }) }.must_raise(RuntimeError)
end
it 'does not support check by mask on Windows' do
resource = MockLoader.new(:windows).load_resource('file', 'C:/fakepath/fakefile')
@ -61,6 +75,7 @@ describe Inspec::Resources::FileResource do
_(resource.readable?('by_usergroup', 'by_specific_user')).must_equal '`readable?` is not supported on your OS yet.'
_(resource.writable?('by_usergroup', 'by_specific_user')).must_equal '`writable?` is not supported on your OS yet.'
_(resource.executable?('by_usergroup', 'by_specific_user')).must_equal '`executable?` is not supported on your OS yet.'
_(resource.allowed?('permission', by: 'by_usergroup', by_user: 'by_specific_user')).must_equal '`allowed?` is not supported on your OS yet.'
proc { resource.send(:contain, nil) }.must_raise(RuntimeError)
end
end