Accept symbols and downcased criteria in aws_iam_policy have_statement matcher (#3129)

Signed-off-by: Clinton Wolfe <clintoncwolfe@gmail.com>
This commit is contained in:
Clinton Wolfe 2018-06-21 14:19:56 -04:00 committed by GitHub
parent 03b6dd8324
commit 44c0fd2e4f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 111 additions and 82 deletions

View file

@ -186,7 +186,7 @@ The test will pass if the identified policy attached the specified role.
Examines the list of statements contained in the policy and passes if at least one of the statements matches. This matcher does _not_ interpret the policy in a request authorization context, as AWS does when a request processed. Rather, `have_statement` examines the literal contents of the IAM policy, and reports on what is present (or absent, when used with `should_not`).
`have_statement` accepts the following criteria to search for matching statements. If any statement matches all the criteria, the test is successful.
`have_statement` accepts the following criteria to search for matching statements. If any statement matches all the criteria, the test is successful. All criteria may be used as Titlecase (as in the AWS examples) or lowercase, string or symbol.
* `Action` - Expresses the requested operation. Acceptable literal values are any AWS operation name, including the '*' wildcard character. `Action` may also use a list of AWS operation names.
* `Effect` - Expresses if the operation is permitted. Acceptable values are 'Deny' and 'Allow'.
@ -204,7 +204,16 @@ Examples:
# Verify there is no full-admin statement
describe aws_iam_policy('kryptonite') do
it { should_not have_statement('Effect' => 'Allow', 'Resource' => '*', 'Action' => '*')}
end
# Symbols and lowercase also allowed as criteria
describe aws_iam_policy('kryptonite') do
# All 4 the same
it { should_not have_statement('Effect' => 'Allow', 'Resource' => '*', 'Action' => '*')}
it { should_not have_statement('effect' => 'Allow', 'resource' => '*', 'action' => '*')}
it { should_not have_statement(Effect: 'Allow', Resource: '*', Action: '*')}
it { should_not have_statement(effect: 'Allow', resource: '*', action: '*')}
end
# Verify bob is allowed to manage things on S3 buckets that start with bobs-stuff

View file

@ -16,6 +16,7 @@ class AwsIamPolicy < Inspec.resource(1)
attr_reader :arn, :attachment_count, :default_version_id
# Note that we also accept downcases and symbol versions of these
EXPECTED_CRITERIA = %w{
Action
Effect
@ -96,7 +97,7 @@ class AwsIamPolicy < Inspec.resource(1)
def has_statement?(provided_criteria = {})
return nil unless exists?
raw_criteria = provided_criteria.dup # provided_criteria is used for output formatting - can't delete from it.
criteria = has_statement__normalize_criteria(has_statement__validate_criteria(raw_criteria))
criteria = has_statement__validate_criteria(raw_criteria)
@normalized_statements ||= has_statement__normalize_statements
statements = has_statement__focus_on_sid(@normalized_statements, criteria)
statements.any? do |statement|
@ -112,15 +113,30 @@ class AwsIamPolicy < Inspec.resource(1)
def has_statement__validate_criteria(raw_criteria)
recognized_criteria = {}
EXPECTED_CRITERIA.each do |expected_criterion|
if raw_criteria.key?(expected_criterion)
recognized_criteria[expected_criterion] = raw_criteria.delete(expected_criterion)
[
expected_criterion,
expected_criterion.downcase,
expected_criterion.to_sym,
expected_criterion.downcase.to_sym,
].each do |variant|
if raw_criteria.key?(variant)
# Always store as downcased symbol
recognized_criteria[expected_criterion.downcase.to_sym] = raw_criteria.delete(variant)
end
end
end
# Special message for valid, but unimplemented statement attributes
UNIMPLEMENTED_CRITERIA.each do |unimplemented_criterion|
if raw_criteria.key?(unimplemented_criterion)
raise ArgumentError, "Criterion '#{unimplemented_criterion}' is not supported for performing have_statement queries."
[
unimplemented_criterion,
unimplemented_criterion.downcase,
unimplemented_criterion.to_sym,
unimplemented_criterion.downcase.to_sym,
].each do |variant|
if raw_criteria.key?(variant)
raise ArgumentError, "Criterion '#{unimplemented_criterion}' is not supported for performing have_statement queries."
end
end
end
@ -130,24 +146,15 @@ class AwsIamPolicy < Inspec.resource(1)
end
# Effect has only 2 permitted values
if recognized_criteria.key?('Effect')
unless %w{Allow Deny}.include?(recognized_criteria['Effect'])
raise ArgumentError, "Criterion 'Effect' for have_statement must be one of 'Allow' or 'Deny' - got '#{recognized_criteria['Effect']}'"
if recognized_criteria.key?(:effect)
unless %w{Allow Deny}.include?(recognized_criteria[:effect])
raise ArgumentError, "Criterion 'Effect' for have_statement must be one of 'Allow' or 'Deny' - got '#{recognized_criteria[:effect]}'"
end
end
recognized_criteria
end
def has_statement__normalize_criteria(criteria)
# Transform keys into lowercase symbols
criteria.keys.each do |provided_key|
criteria[provided_key.downcase.to_sym] = criteria.delete(provided_key)
end
criteria
end
def has_statement__normalize_statements
# Some single-statement policies place their statement
# directly in policy['Statement'], rather than in an

View file

@ -164,7 +164,14 @@ class AwsIamPolicyMatchersTest < Minitest::Test
'Resource' => 'dummy',
'Sid' => 'dummy',
}.each do |criterion, test_value|
AwsIamPolicy.new('test-policy-1').has_statement?(criterion => test_value)
[
criterion,
criterion.downcase,
criterion.to_sym,
criterion.downcase.to_sym
].each do |variant|
AwsIamPolicy.new('test-policy-1').has_statement?(variant => test_value)
end
end
end
@ -187,79 +194,85 @@ class AwsIamPolicyMatchersTest < Minitest::Test
end
def test_have_statement_when_sid_is_provided
assert(AwsIamPolicy.new('test-policy-1').has_statement?('Sid' => 'beta01'))
assert(AwsIamPolicy.new('test-policy-2').has_statement?('Sid' => 'CloudWatchEventsFullAccess'))
assert(AwsIamPolicy.new('test-policy-2').has_statement?('Sid' => 'IAMPassRoleForCloudWatchEvents'))
refute(AwsIamPolicy.new('test-policy-2').has_statement?('Sid' => 'beta01'))
['Sid', 'sid', :Sid, :sid].each do |variant|
assert(AwsIamPolicy.new('test-policy-1').has_statement?(variant => 'beta01'))
assert(AwsIamPolicy.new('test-policy-2').has_statement?(variant => 'CloudWatchEventsFullAccess'))
assert(AwsIamPolicy.new('test-policy-2').has_statement?(variant => 'IAMPassRoleForCloudWatchEvents'))
refute(AwsIamPolicy.new('test-policy-2').has_statement?(variant => 'beta01'))
assert(AwsIamPolicy.new('test-policy-1').has_statement?('Sid' => /eta/))
assert(AwsIamPolicy.new('test-policy-2').has_statement?('Sid' => /CloudWatch/))
refute(AwsIamPolicy.new('test-policy-2').has_statement?('Sid' => /eta/))
end
def test_have_statement_when_provided_invalid_effect
assert_raises(ArgumentError) { AwsIamPolicy.new('test-policy-1').has_statement?('Effect' => 'Disallow') }
assert_raises(ArgumentError) { AwsIamPolicy.new('test-policy-1').has_statement?('Effect' => 'allow') }
assert_raises(ArgumentError) { AwsIamPolicy.new('test-policy-1').has_statement?('Effect' => :Allow) }
assert_raises(ArgumentError) { AwsIamPolicy.new('test-policy-1').has_statement?('Effect' => :allow) }
assert(AwsIamPolicy.new('test-policy-1').has_statement?(variant => /eta/))
assert(AwsIamPolicy.new('test-policy-2').has_statement?(variant => /CloudWatch/))
refute(AwsIamPolicy.new('test-policy-2').has_statement?(variant => /eta/))
end
end
def test_have_statement_when_effect_is_provided
assert(AwsIamPolicy.new('test-policy-1').has_statement?('Effect' => 'Deny'))
refute(AwsIamPolicy.new('test-policy-1').has_statement?('Effect' => 'Allow'))
assert(AwsIamPolicy.new('test-policy-2').has_statement?('Effect' => 'Allow'))
['Effect','effect',:Effect,:effect].each do |variant|
assert(AwsIamPolicy.new('test-policy-1').has_statement?(variant => 'Deny'))
refute(AwsIamPolicy.new('test-policy-1').has_statement?(variant => 'Allow'))
assert(AwsIamPolicy.new('test-policy-2').has_statement?(variant => 'Allow'))
assert_raises(ArgumentError) { AwsIamPolicy.new('test-policy-1').has_statement?(variant => 'Disallow') }
assert_raises(ArgumentError) { AwsIamPolicy.new('test-policy-1').has_statement?(variant => 'allow') }
assert_raises(ArgumentError) { AwsIamPolicy.new('test-policy-1').has_statement?(variant => :Allow) }
assert_raises(ArgumentError) { AwsIamPolicy.new('test-policy-1').has_statement?(variant => :allow) }
end
end
def test_have_statement_when_action_is_provided
# Able to match a simple string action when multiple statements present
assert(AwsIamPolicy.new('test-policy-2').has_statement?('Action' => 'iam:PassRole'))
# Able to match a wildcard string action
assert(AwsIamPolicy.new('test-policy-2').has_statement?('Action' => 'events:*'))
# Do not match a wildcard when using strings
refute(AwsIamPolicy.new('test-policy-2').has_statement?('Action' => 'events:EnableRule'))
# Do match when using a regex
assert(AwsIamPolicy.new('test-policy-2').has_statement?('Action' => /^events\:/))
# Able to match one action when the statement has an array of actions
assert(AwsIamPolicy.new('test-policy-1').has_statement?('Action' => 'ec2:DescribeSubnets'))
# Do not match if only one action specified as an array when the statement has an array of actions
refute(AwsIamPolicy.new('test-policy-1').has_statement?('Action' => ['ec2:DescribeSubnets']))
# Do match if two actions specified when the statement has an array of actions
assert(AwsIamPolicy.new('test-policy-1').has_statement?('Action' => ['ec2:DescribeSubnets', 'ec2:DescribeSecurityGroups']))
# Do match setwise if two actions specified when the statement has an array of actions
assert(AwsIamPolicy.new('test-policy-1').has_statement?('Action' => ['ec2:DescribeSecurityGroups', 'ec2:DescribeSubnets']))
# Do match if only one regex action specified when the statement has an array of actions
assert(AwsIamPolicy.new('test-policy-1').has_statement?('Action' => /^ec2\:Describe/))
# Do match if one regex action specified in an array when the statement has an array of actions
assert(AwsIamPolicy.new('test-policy-1').has_statement?('Action' => [/^ec2\:Describe/]))
# Able to match a degenerate policy doc in which there is exactly one statement as a hash.
assert(AwsIamPolicy.new('test-policy-3').has_statement?('Action' => 'acm:GetCertificate'))
# Don't explode, and also don't match, if a policy has a statement without an Action
refute(AwsIamPolicy.new('test-policy-4').has_statement?('Action' => 'iam:*'))
['Action', 'action', :Action, :action].each do |variant|
# Able to match a simple string action when multiple statements present
assert(AwsIamPolicy.new('test-policy-2').has_statement?(variant => 'iam:PassRole'))
# Able to match a wildcard string action
assert(AwsIamPolicy.new('test-policy-2').has_statement?(variant => 'events:*'))
# Do not match a wildcard when using strings
refute(AwsIamPolicy.new('test-policy-2').has_statement?(variant => 'events:EnableRule'))
# Do match when using a regex
assert(AwsIamPolicy.new('test-policy-2').has_statement?(variant => /^events\:/))
# Able to match one action when the statement has an array of actions
assert(AwsIamPolicy.new('test-policy-1').has_statement?(variant => 'ec2:DescribeSubnets'))
# Do not match if only one action specified as an array when the statement has an array of actions
refute(AwsIamPolicy.new('test-policy-1').has_statement?(variant => ['ec2:DescribeSubnets']))
# Do match if two actions specified when the statement has an array of actions
assert(AwsIamPolicy.new('test-policy-1').has_statement?(variant => ['ec2:DescribeSubnets', 'ec2:DescribeSecurityGroups']))
# Do match setwise if two actions specified when the statement has an array of actions
assert(AwsIamPolicy.new('test-policy-1').has_statement?(variant => ['ec2:DescribeSecurityGroups', 'ec2:DescribeSubnets']))
# Do match if only one regex action specified when the statement has an array of actions
assert(AwsIamPolicy.new('test-policy-1').has_statement?(variant => /^ec2\:Describe/))
# Do match if one regex action specified in an array when the statement has an array of actions
assert(AwsIamPolicy.new('test-policy-1').has_statement?(variant => [/^ec2\:Describe/]))
# Able to match a degenerate policy doc in which there is exactly one statement as a hash.
assert(AwsIamPolicy.new('test-policy-3').has_statement?(variant => 'acm:GetCertificate'))
# Don't explode, and also don't match, if a policy has a statement without an Action
refute(AwsIamPolicy.new('test-policy-4').has_statement?(variant => 'iam:*'))
end
end
def test_have_statement_when_resource_is_provided
# Able to match a simple string resource when multiple statements present
assert(AwsIamPolicy.new('test-policy-2').has_statement?('Resource' => 'arn:aws:iam::*:role/AWS_Events_Invoke_Targets'))
# Able to match a wildcard string resource
assert(AwsIamPolicy.new('test-policy-2').has_statement?('Resource' => '*'))
# Do not match a wildcard when using strings
refute(AwsIamPolicy.new('test-policy-2').has_statement?('Resource' => 'arn:aws:events:us-east-1:123456789012:rule/my-rule'))
# Do match when using a regex
assert(AwsIamPolicy.new('test-policy-2').has_statement?('Resource' => /AWS_Events_Invoke_Targets$/))
# Able to match one resource when the statement has an array of resources
assert(AwsIamPolicy.new('test-policy-1').has_statement?('Resource' => 'arn:aws:ec2:::*'))
# Do not match if only one resource specified as an array when the statement has an array of resources
refute(AwsIamPolicy.new('test-policy-1').has_statement?('Resource' => ['arn:aws:ec2:::*']))
# Do match if two resources specified when the statement has an array of resources
assert(AwsIamPolicy.new('test-policy-1').has_statement?('Resource' => ['arn:aws:ec2:::*', '*']))
# Do match setwise if two resources specified when the statement has an array of resources
assert(AwsIamPolicy.new('test-policy-1').has_statement?('Resource' => ['*', 'arn:aws:ec2:::*']))
# Do match if only one regex resource specified when the statement has an array of resources
assert(AwsIamPolicy.new('test-policy-1').has_statement?('Resource' => /^arn\:aws\:ec2/))
# Do match if one regex resource specified in an array when the statement has an array of resources
assert(AwsIamPolicy.new('test-policy-1').has_statement?('Resource' => [/\*/]))
# Able to match a degenerate policy doc in which there is exactly one statement as a hash.
assert(AwsIamPolicy.new('test-policy-3').has_statement?('Resource' => '*'))
['Resource', 'resource', :Resource, :resource].each do |variant|
# Able to match a simple string resource when multiple statements present
assert(AwsIamPolicy.new('test-policy-2').has_statement?(variant => 'arn:aws:iam::*:role/AWS_Events_Invoke_Targets'))
# Able to match a wildcard string resource
assert(AwsIamPolicy.new('test-policy-2').has_statement?(variant => '*'))
# Do not match a wildcard when using strings
refute(AwsIamPolicy.new('test-policy-2').has_statement?(variant => 'arn:aws:events:us-east-1:123456789012:rule/my-rule'))
# Do match when using a regex
assert(AwsIamPolicy.new('test-policy-2').has_statement?(variant => /AWS_Events_Invoke_Targets$/))
# Able to match one resource when the statement has an array of resources
assert(AwsIamPolicy.new('test-policy-1').has_statement?(variant => 'arn:aws:ec2:::*'))
# Do not match if only one resource specified as an array when the statement has an array of resources
refute(AwsIamPolicy.new('test-policy-1').has_statement?(variant => ['arn:aws:ec2:::*']))
# Do match if two resources specified when the statement has an array of resources
assert(AwsIamPolicy.new('test-policy-1').has_statement?(variant => ['arn:aws:ec2:::*', '*']))
# Do match setwise if two resources specified when the statement has an array of resources
assert(AwsIamPolicy.new('test-policy-1').has_statement?(variant => ['*', 'arn:aws:ec2:::*']))
# Do match if only one regex resource specified when the statement has an array of resources
assert(AwsIamPolicy.new('test-policy-1').has_statement?(variant => /^arn\:aws\:ec2/))
# Do match if one regex resource specified in an array when the statement has an array of resources
assert(AwsIamPolicy.new('test-policy-1').has_statement?(variant => [/\*/]))
# Able to match a degenerate policy doc in which there is exactly one statement as a hash.
assert(AwsIamPolicy.new('test-policy-3').has_statement?(variant => '*'))
end
end
end