WIP: chain: Introduce new condition operations #93

Closed
aarifullin wants to merge 1 commit from aarifullin/policy-engine:feat/cond_if_exists into master
4 changed files with 405 additions and 24 deletions

View file

@ -111,6 +111,20 @@ const (
CondIPAddress CondIPAddress
CondNotIPAddress CondNotIPAddress
CondStringEqualsIfExists
CondStringEqualsIgnoreCaseIfExists
CondStringLikeIfExists
CondStringLessThanIfExists
CondStringLessThanEqualsIfExists
CondStringGreaterThanIfExists
CondStringGreaterThanEqualsIfExists
CondNumericEqualsIfExists
CondNumericLessThanIfExists
CondNumericLessThanEqualsIfExists
CondNumericGreaterThanIfExists
CondNumericGreaterThanEqualsIfExists
) )
var condToStr = []struct { var condToStr = []struct {
@ -127,12 +141,24 @@ var condToStr = []struct {
{CondStringLessThanEquals, "StringLessThanEquals"}, {CondStringLessThanEquals, "StringLessThanEquals"},
{CondStringGreaterThan, "StringGreaterThan"}, {CondStringGreaterThan, "StringGreaterThan"},
{CondStringGreaterThanEquals, "StringGreaterThanEquals"}, {CondStringGreaterThanEquals, "StringGreaterThanEquals"},
{CondStringEqualsIfExists, "StringEqualsIfExists"},
{CondStringEqualsIgnoreCaseIfExists, "StringEqualsIgnoreCaseIfExists"},
{CondStringLikeIfExists, "StringLikeIfExists"},
{CondStringLessThanIfExists, "StringLessThanIfExists"},
{CondStringLessThanEqualsIfExists, "StringLessThanEqualsIfExists"},
{CondStringGreaterThanIfExists, "StringGreaterThanIfExists"},
{CondStringGreaterThanEqualsIfExists, "StringGreaterThanEqualsIfExists"},
{CondNumericEquals, "NumericEquals"}, {CondNumericEquals, "NumericEquals"},
{CondNumericNotEquals, "NumericNotEquals"}, {CondNumericNotEquals, "NumericNotEquals"},
{CondNumericLessThan, "NumericLessThan"}, {CondNumericLessThan, "NumericLessThan"},
{CondNumericLessThanEquals, "NumericLessThanEquals"}, {CondNumericLessThanEquals, "NumericLessThanEquals"},
{CondNumericGreaterThan, "NumericGreaterThan"}, {CondNumericGreaterThan, "NumericGreaterThan"},
{CondNumericGreaterThanEquals, "NumericGreaterThanEquals"}, {CondNumericGreaterThanEquals, "NumericGreaterThanEquals"},
{CondNumericEqualsIfExists, "NumericEqualsIfExists"},
{CondNumericLessThanIfExists, "NumericLessThanIfExists"},
{CondNumericLessThanEqualsIfExists, "NumericLessThanEqualsIfExists"},
{CondNumericGreaterThanIfExists, "NumericGreaterThanIfExists"},
{CondNumericGreaterThanEqualsIfExists, "NumericGreaterThanEqualsIfExists"},
{CondSliceContains, "SliceContains"}, {CondSliceContains, "SliceContains"},
{CondIPAddress, "IPAddress"}, {CondIPAddress, "IPAddress"},
{CondNotIPAddress, "NotIPAddress"}, {CondNotIPAddress, "NotIPAddress"},
@ -157,11 +183,12 @@ func FormCondSliceContainsValue(values []string) string {
func (c *Condition) Match(req resource.Request) bool { func (c *Condition) Match(req resource.Request) bool {
var val string var val string
var exists bool
switch c.Kind { switch c.Kind {
case KindResource: case KindResource:
val = req.Resource().Property(c.Key) val, exists = req.Resource().Property(c.Key)
case KindRequest: case KindRequest:
val = req.Property(c.Key) val, exists = req.Property(c.Key)
default: default:
panic(fmt.Sprintf("unknown condition type: %d", c.Kind)) panic(fmt.Sprintf("unknown condition type: %d", c.Kind))
} }
@ -170,30 +197,47 @@ func (c *Condition) Match(req resource.Request) bool {
default: default:
panic(fmt.Sprintf("unimplemented: %d", c.Op)) panic(fmt.Sprintf("unimplemented: %d", c.Op))
case CondStringEquals: case CondStringEquals:
return val == c.Value return exists && val == c.Value
Review

Why do we change this?

Why do we change this?
Review
https://git.frostfs.info/TrueCloudLab/policy-engine/pulls/93#issuecomment-55390
case CondStringEqualsIfExists:
return !exists || val == c.Value
case CondStringNotEquals: case CondStringNotEquals:
return val != c.Value return exists && val != c.Value
Review

Why have you changed this?

Why have you changed this?
Review

The reason why this PR has been created:
CondStringNotEquals works out even key is not present (`"" != value -> true) - and we don't want this complicating behavior. If we don't need these changes, then the PR is not needed at all and policy must explicitly specify that a value (got by a key) can't be empty

The [reason](https://git.frostfs.info/TrueCloudLab/frostfs-node/issues/1406#issuecomment-53462) why this PR has been created: `CondStringNotEquals` works out even key **is not** present (`"" != value -> true) - and we don't want this complicating behavior. If we don't need these changes, then the PR is not needed at all and policy must explicitly specify that a value (got by a key) can't be empty
Review

We need this change for positive conditions, not the negative ones.
As you can see below, negative conditions are handled differently.

We need this change for positive conditions, not the negative ones. As you can see below, negative conditions are handled differently.
Review

Oh, sorry! I misunderstood this - have been keeping the idea about ...IfExists operators in my mind

Oh, sorry! I misunderstood this - have been keeping the idea about `...IfExists` operators in my mind
case CondStringEqualsIgnoreCase: case CondStringEqualsIgnoreCase:
return strings.EqualFold(val, c.Value) return exists && strings.EqualFold(val, c.Value)
case CondStringEqualsIgnoreCaseIfExists:
return !exists || strings.EqualFold(val, c.Value)
case CondStringNotEqualsIgnoreCase: case CondStringNotEqualsIgnoreCase:
return !strings.EqualFold(val, c.Value) return exists && !strings.EqualFold(val, c.Value)
case CondStringLike: case CondStringLike:
return util.GlobMatch(val, c.Value) return exists && util.GlobMatch(val, c.Value)
case CondStringLikeIfExists:
return !exists || util.GlobMatch(val, c.Value)

@alexvanin @dkirillov could you recheck this bit?

I am baffled by this sentence from https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html#Conditions_IfExists:

If you are using an "Effect": "Deny" element with a negated condition operator like StringNotEqualsIfExists, the request is still denied even if the condition key is not present.

It seems we behave differently. What am I missing?

@alexvanin @dkirillov could you recheck this bit? I am baffled by this sentence from https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html#Conditions_IfExists: > If you are using an "Effect": "Deny" element with a negated condition operator like StringNotEqualsIfExists, the request is still denied even if the condition key is not present. It seems we behave differently. What am I missing?
   {
     "Effect": "Deny",
     "Action": "s3:PutObject",
     "Resource": "*",
     "Condition": {
       "StringNotEquals": {
         "s3:x-amz-server-side-encryption": "AES256"
       }
     }
   }

In current implementation:

  1. If the property s3:x-amz-server-side-encryption is not set, then val_found && StringNotEquals(val, "s3:x-amz-server-side-encryption") -> false that seems fair;
  2. If the property s3:x-amz-server-side-encryption is set, then val_found && StringNotEquals(val, "s3:x-amz-server-side-encryption") -> true/false that seems fair;
  3. If the property s3:x-amz-server-side-encryption is not set, then StringNotEqualsIfExists(...) -> !val_found || StringNotEquals(val, "s3:x-amz-server-side-encryption") -> true
  4. If the property s3:x-amz-server-side-encryption is set, then StringNotEqualsIfExists(...) -> !val_found || StringNotEquals(val, "s3:x-amz-server-side-encryption") -> true/false. If "s3:x-amz-server-side-encryption" is set to "", then it returns true

Anyway, the current implementation fits to this requirement even for StringNotEquals:

If you are using an "Effect": "Deny" element with a negated condition operator like StringNotEqualsIfExists, the request is still denied even if the condition key is not present.

BTW, this is a consequence of this statement IfExists" suffix specifies the following: if the condition key is present in the context of the request, process the key as specified in the policy. If the key is not present, evaluate the condition element as true

```json { "Effect": "Deny", "Action": "s3:PutObject", "Resource": "*", "Condition": { "StringNotEquals": { "s3:x-amz-server-side-encryption": "AES256" } } } ``` In current implementation: 1. If the property `s3:x-amz-server-side-encryption` is _not_ set, then `val_found && StringNotEquals(val, "s3:x-amz-server-side-encryption") -> false` that seems fair; 2. If the property `s3:x-amz-server-side-encryption` is set, then `val_found && StringNotEquals(val, "s3:x-amz-server-side-encryption") -> true/false` that seems fair; 3. If the property `s3:x-amz-server-side-encryption` is **not** set, then `StringNotEqualsIfExists(...) -> !val_found || StringNotEquals(val, "s3:x-amz-server-side-encryption") -> true` 4. If the property `s3:x-amz-server-side-encryption` is set, then `StringNotEqualsIfExists(...) -> !val_found || StringNotEquals(val, "s3:x-amz-server-side-encryption") -> true/false`. If `"s3:x-amz-server-side-encryption"` is set to `""`, then it returns `true` Anyway, the current implementation fits to this requirement even for `StringNotEquals`: ``` If you are using an "Effect": "Deny" element with a negated condition operator like StringNotEqualsIfExists, the request is still denied even if the condition key is not present. ``` BTW, this is a consequence of this statement `IfExists" suffix specifies the following: if the condition key is present in the context of the request, process the key as specified in the policy. If the key is not present, evaluate the condition element as true`

If the property s3:x-amz-server-side-encryption is not set, then val_found && StringNotEquals(val, "s3:x-amz-server-side-encryption") -> false that seems fair;

I'm not sure. The behavior being changed in this PR. Currently such condition evaluates to true because missing key has "" (empty) value that doesn't equal to "AES256" (in example)

We shouldn't change this behavior I suppose. Moreover AWS behaves the same way

Using this policy

{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Deny",
    "Principal": "*",
    "Action": "s3:PutObject",
    "Resource": "arn:aws:s3:::bucket/*",
    "Condition": {
      "StringNotEquals": {
        "s3:RequestObjectTag/environment": "production"
      }
    }
  }
}

I can put object only if provide tag environment=production in request

> If the property s3:x-amz-server-side-encryption is not set, then val_found && StringNotEquals(val, "s3:x-amz-server-side-encryption") -> false that seems fair; I'm not sure. The behavior being changed in this PR. Currently such condition evaluates to `true` because missing key has "" (empty) value that doesn't equal to "AES256" (in example) We shouldn't change this behavior I suppose. Moreover AWS behaves the same way Using this policy ```json { "Version": "2012-10-17", "Statement": { "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringNotEquals": { "s3:RequestObjectTag/environment": "production" } } } } ``` I can put object only if provide tag `environment=production` in request

It it difficult to add a test here and check with other implementations to be sure?
aa82bd16ae/s3tests_boto3/functional/test_s3.py

It it difficult to add a test here and check with other implementations to be sure? https://git.frostfs.info/TrueCloudLab/s3-tests/src/commit/aa82bd16aef7c8b37cbc5e95ee06ff198d6270a8/s3tests_boto3/functional/test_s3.py

I'm not sure. The behavior being changed in this PR. Currently such condition evaluates to true because missing key has "" (empty) value that doesn't equal to "AES256" (in example)

Probably, I incorrectly pointed to "current implementation". I meant - "current" like in this PR, not master

> I'm not sure. The behavior being changed in this PR. Currently such condition evaluates to true because missing key has "" (empty) value that doesn't equal to "AES256" (in example) Probably, I incorrectly pointed to "current implementation". I meant - "current" like in this PR, not `master`

It it difficult to add a test here and check with other implementations to be sure?
aa82bd16ae/s3tests_boto3/functional/test_s3.py

I suppose it's possible, but first we need to know what policies we are about to test

> It it difficult to add a test here and check with other implementations to be sure? aa82bd16ae/s3tests_boto3/functional/test_s3.py I suppose it's possible, but first we need to know what policies we are about to test
TEST 1
        {"Version": "2012-10-17","Statement": {"Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringNotEquals":{"s3:RequestObjectTag/environment": "production"}}}}
        ENV: s3:RequestObjectTag/environment: production
TEST 2
        {"Version": "2012-10-17","Statement": {"Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringNotEquals":{"s3:RequestObjectTag/environment": "production"}}}}
        ENV: s3:RequestObjectTag/environment: development
TEST 3
        {"Version": "2012-10-17","Statement": {"Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringNotEquals":{"s3:RequestObjectTag/environment": "production"}}}}
        ENV: s3:RequestObjectTag/environment: <MISSING>
TEST 4
        {"Version": "2012-10-17","Statement": {"Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringEquals":{"s3:RequestObjectTag/environment": "production"}}}}
        ENV: s3:RequestObjectTag/environment: production
TEST 5
        {"Version": "2012-10-17","Statement": {"Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringEquals":{"s3:RequestObjectTag/environment": "production"}}}}
        ENV: s3:RequestObjectTag/environment: development
TEST 6
        {"Version": "2012-10-17","Statement": {"Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringEquals":{"s3:RequestObjectTag/environment": "production"}}}}
        ENV: s3:RequestObjectTag/environment: <MISSING>
TEST 7
        {"Version": "2012-10-17","Statement": {"Effect": "Deny","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringNotEquals":{"s3:RequestObjectTag/environment": "production"}}}}
        ENV: s3:RequestObjectTag/environment: production
TEST 8
        {"Version": "2012-10-17","Statement": {"Effect": "Deny","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringNotEquals":{"s3:RequestObjectTag/environment": "production"}}}}
        ENV: s3:RequestObjectTag/environment: development
TEST 9
        {"Version": "2012-10-17","Statement": {"Effect": "Deny","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringNotEquals":{"s3:RequestObjectTag/environment": "production"}}}}
        ENV: s3:RequestObjectTag/environment: <MISSING>
TEST 10
        {"Version": "2012-10-17","Statement": {"Effect": "Deny","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringEquals":{"s3:RequestObjectTag/environment": "production"}}}}
        ENV: s3:RequestObjectTag/environment: production
TEST 11
        {"Version": "2012-10-17","Statement": {"Effect": "Deny","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringEquals":{"s3:RequestObjectTag/environment": "production"}}}}
        ENV: s3:RequestObjectTag/environment: development
TEST 12
        {"Version": "2012-10-17","Statement": {"Effect": "Deny","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringEquals":{"s3:RequestObjectTag/environment": "production"}}}}
        ENV: s3:RequestObjectTag/environment: <MISSING>
``` TEST 1 {"Version": "2012-10-17","Statement": {"Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringNotEquals":{"s3:RequestObjectTag/environment": "production"}}}} ENV: s3:RequestObjectTag/environment: production TEST 2 {"Version": "2012-10-17","Statement": {"Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringNotEquals":{"s3:RequestObjectTag/environment": "production"}}}} ENV: s3:RequestObjectTag/environment: development TEST 3 {"Version": "2012-10-17","Statement": {"Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringNotEquals":{"s3:RequestObjectTag/environment": "production"}}}} ENV: s3:RequestObjectTag/environment: <MISSING> TEST 4 {"Version": "2012-10-17","Statement": {"Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringEquals":{"s3:RequestObjectTag/environment": "production"}}}} ENV: s3:RequestObjectTag/environment: production TEST 5 {"Version": "2012-10-17","Statement": {"Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringEquals":{"s3:RequestObjectTag/environment": "production"}}}} ENV: s3:RequestObjectTag/environment: development TEST 6 {"Version": "2012-10-17","Statement": {"Effect": "Allow","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringEquals":{"s3:RequestObjectTag/environment": "production"}}}} ENV: s3:RequestObjectTag/environment: <MISSING> TEST 7 {"Version": "2012-10-17","Statement": {"Effect": "Deny","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringNotEquals":{"s3:RequestObjectTag/environment": "production"}}}} ENV: s3:RequestObjectTag/environment: production TEST 8 {"Version": "2012-10-17","Statement": {"Effect": "Deny","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringNotEquals":{"s3:RequestObjectTag/environment": "production"}}}} ENV: s3:RequestObjectTag/environment: development TEST 9 {"Version": "2012-10-17","Statement": {"Effect": "Deny","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringNotEquals":{"s3:RequestObjectTag/environment": "production"}}}} ENV: s3:RequestObjectTag/environment: <MISSING> TEST 10 {"Version": "2012-10-17","Statement": {"Effect": "Deny","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringEquals":{"s3:RequestObjectTag/environment": "production"}}}} ENV: s3:RequestObjectTag/environment: production TEST 11 {"Version": "2012-10-17","Statement": {"Effect": "Deny","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringEquals":{"s3:RequestObjectTag/environment": "production"}}}} ENV: s3:RequestObjectTag/environment: development TEST 12 {"Version": "2012-10-17","Statement": {"Effect": "Deny","Principal": "*","Action": "s3:PutObject","Resource": "arn:aws:s3:::bucket/*","Condition":{"StringEquals":{"s3:RequestObjectTag/environment": "production"}}}} ENV: s3:RequestObjectTag/environment: <MISSING> ```

Currently I have results only for deny policies. In AWS there are some problem make bucket public.

  • TEST 7: not matched
  • TEST 8: matched
  • TEST 9: matched
  • TEST 10: matched
  • TEST 11: not matched
  • TEST 12: not matched

These results for AWS.
Yandex badly support policies and we always get AccessDenied

Currently I have results only for deny policies. In AWS there are some problem make bucket public. * TEST 7: not matched * TEST 8: matched * TEST 9: matched * TEST 10: matched * TEST 11: not matched * TEST 12: not matched These results for AWS. Yandex badly support policies and we always get AccessDenied

I couldn't validate policies with Allow statement and PutObject but I could validate it with PutObjectTagging.
So policy was

{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Principal": "*",
    "Action": "s3:PutObjectTagging",
    "Resource": [
        "arn:aws:s3:::bucket",
        "arn:aws:s3:::bucket/*"
    ],
    "Condition":{
        "StringEquals": {
            "s3:RequestObjectTag/Environment": "production"
        }
    }
  }
}

Results:

  • TEST 1: not matched -> AccessDenied
  • TEST 2: matched -> Allow
  • TEST 3: matched -> Allow
  • TEST 4: matched -> Allow
  • TEST 5: not matched -> AccessDenied
  • TEST 6: not matched -> AccessDenied
I couldn't validate policies with Allow statement and `PutObject` but I could validate it with `PutObjectTagging`. So policy was ```json { "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": "*", "Action": "s3:PutObjectTagging", "Resource": [ "arn:aws:s3:::bucket", "arn:aws:s3:::bucket/*" ], "Condition":{ "StringEquals": { "s3:RequestObjectTag/Environment": "production" } } } } ``` Results: * TEST 1: not matched -> AccessDenied * TEST 2: matched -> Allow * TEST 3: matched -> Allow * TEST 4: matched -> Allow * TEST 5: not matched -> AccessDenied * TEST 6: not matched -> AccessDenied
branch with tests https://git.frostfs.info/dkirillov/s3-tests/src/branch/feature/ape_if_exists_conditions

This clarifies the quoted sentence from the IAM doc:

  1. TEST 3: matched -> Allow, this means that if the attribute is missing then NotEquals treats it as if it was empty. Which is logical, given that tests 4-6 have the opposite results (and the opposite operation).

For tests 7-12 (ALLOW) we have the behaviour similar to (DENY) in terms of matching.

But I don't understand what is the different between StringNotEquals and StringNotEqualsIfExists?
Probably IfExists will NOT match in the test 3?

Here are the same tests, but for IfExists, could you check them too?

TEST 1
{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringNotEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } }
        ENV: s3:RequestObjectTag/environment: production
TEST 2
{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringNotEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } }
        ENV: s3:RequestObjectTag/environment: development
TEST 3
{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringNotEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } }
        ENV: s3:RequestObjectTag/environment: <MISSING>
TEST 4
{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } }
        ENV: s3:RequestObjectTag/environment: production
TEST 5
{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } }
        ENV: s3:RequestObjectTag/environment: development
TEST 6
{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } }
        ENV: s3:RequestObjectTag/environment: <MISSING>
TEST 7
{ "Version": "2012-10-17", "Statement": { "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringNotEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } }
        ENV: s3:RequestObjectTag/environment: production
TEST 8
{ "Version": "2012-10-17", "Statement": { "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringNotEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } }
        ENV: s3:RequestObjectTag/environment: development
TEST 9
{ "Version": "2012-10-17", "Statement": { "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringNotEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } }
        ENV: s3:RequestObjectTag/environment: <MISSING>
TEST 10
{ "Version": "2012-10-17", "Statement": { "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } }
        ENV: s3:RequestObjectTag/environment: production
TEST 11
{ "Version": "2012-10-17", "Statement": { "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } }
        ENV: s3:RequestObjectTag/environment: development
TEST 12
{ "Version": "2012-10-17", "Statement": { "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } }
        ENV: s3:RequestObjectTag/environment: <MISSING>
This clarifies the quoted sentence from the IAM doc: 1. `TEST 3: matched -> Allow`, this means that if the attribute is missing then `NotEquals` treats it as if it was empty. Which is logical, given that tests 4-6 have the opposite results (and the opposite operation). For tests 7-12 (ALLOW) we have the behaviour similar to (DENY) in terms of matching. But I don't understand what is the different between `StringNotEquals` and `StringNotEqualsIfExists`? Probably `IfExists` will NOT match in the test 3? Here are the same tests, but for `IfExists`, could you check them too? ``` TEST 1 { "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringNotEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } } ENV: s3:RequestObjectTag/environment: production TEST 2 { "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringNotEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } } ENV: s3:RequestObjectTag/environment: development TEST 3 { "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringNotEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } } ENV: s3:RequestObjectTag/environment: <MISSING> TEST 4 { "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } } ENV: s3:RequestObjectTag/environment: production TEST 5 { "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } } ENV: s3:RequestObjectTag/environment: development TEST 6 { "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } } ENV: s3:RequestObjectTag/environment: <MISSING> TEST 7 { "Version": "2012-10-17", "Statement": { "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringNotEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } } ENV: s3:RequestObjectTag/environment: production TEST 8 { "Version": "2012-10-17", "Statement": { "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringNotEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } } ENV: s3:RequestObjectTag/environment: development TEST 9 { "Version": "2012-10-17", "Statement": { "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringNotEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } } ENV: s3:RequestObjectTag/environment: <MISSING> TEST 10 { "Version": "2012-10-17", "Statement": { "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } } ENV: s3:RequestObjectTag/environment: production TEST 11 { "Version": "2012-10-17", "Statement": { "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } } ENV: s3:RequestObjectTag/environment: development TEST 12 { "Version": "2012-10-17", "Statement": { "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringEqualsIfExists": { "s3:RequestObjectTag/environment": "production" } } } } ENV: s3:RequestObjectTag/environment: <MISSING> ```

The same as previous (TEST 1-6 used PutObjectTagging instead of PutObject, TEST 7-12 used PubObject)

  • TEST 1: not matched -> AccessDenied
  • TEST 2: matched -> Allow
  • TEST 3: matched -> Allow
  • TEST 4: matched -> Allow
  • TEST 5: not matched -> AccessDenied
  • TEST 6: matched -> Allow
  • TEST 7: not matched -> Allow
  • TEST 8: matched -> AccessDenied
  • TEST 9: matched -> AccessDenied
  • TEST 10: matched -> AccessDenied
  • TEST 11: not matched -> Allow
  • TEST 12: matched -> AccessDenied

P.S. For the history tests 1-6 for the PutObject (It looks weird, probably I missed something)

  • TEST 1: not matched -> AccessDenied
  • TEST 2: not matched -> AccessDenied
  • TEST 3: matched -> Allow
  • TEST 4: not matched -> AccessDenied
  • TEST 5: not matched -> AccessDenied
  • TEST 6: not matched -> AccessDenied
The same as previous (TEST 1-6 used PutObjectTagging instead of PutObject, TEST 7-12 used PubObject) * TEST 1: not matched -> AccessDenied * TEST 2: matched -> Allow * TEST 3: matched -> Allow * TEST 4: matched -> Allow * TEST 5: not matched -> AccessDenied * TEST 6: matched -> Allow * * TEST 7: not matched -> Allow * TEST 8: matched -> AccessDenied * TEST 9: matched -> AccessDenied * TEST 10: matched -> AccessDenied * TEST 11: not matched -> Allow * TEST 12: matched -> AccessDenied P.S. For the history tests 1-6 for the PutObject (It looks weird, probably I missed something) * TEST 1: not matched -> AccessDenied * TEST 2: not matched -> AccessDenied * TEST 3: matched -> Allow * TEST 4: not matched -> AccessDenied * TEST 5: not matched -> AccessDenied * TEST 6: not matched -> AccessDenied

So we have different results in test 6

Tests 7-12 seem to differ a lot from your previous comment, is there some copy-paste error here?

So we have different results in test 6 Tests 7-12 seem to differ a lot from your previous comment, is there some copy-paste error here?

No, there shouldn't be any mistakes. As for me we got the same match result for pair tests

  • TEST 1,7 - not matched
  • TEST 2,8 - matched
  • TEST 3,9 - matched
  • TEST 4,10 - matched
  • TEST 5,11 - not matched
  • TEST 6,12 - matched

and it's expected in general (I means symmetric results for Allow/Deny Equals/NotEquals).

No, there shouldn't be any mistakes. As for me we got the same match result for pair tests * TEST 1,7 - not matched * TEST 2,8 - matched * TEST 3,9 - matched * TEST 4,10 - matched * TEST 5,11 - not matched * TEST 6,12 - matched and it's expected in general (I means symmetric results for Allow/Deny Equals/NotEquals).

So NotEquals doesn't differ from NotEqualsIfExists, right?
Equals differs from EqualsIfExists, though.

So `NotEquals` doesn't differ from `NotEqualsIfExists`, right? `Equals` differs from `EqualsIfExists`, though.

Hm, I think I have found the discriminator:

{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*",
  "Condition": {
    "StringNotEqualsIfExists": { "s3:RequestObjectTag/environment": "production" },
    "StringNotEqualsIfExists": { "s3:RequestObjectTag/anotherparameter": "xxx" }
   }}}
        ENV:
        s3:RequestObjectTag/environment: development
        s3:RequestObjectTag/anotherparameter: xxx

DISCRIMINATOR 2:
{ "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*",
  "Condition": {
    "StringNotEqualsIfExists": { "s3:RequestObjectTag/environment": "production" },
    "StringNotEqualsIfExists": { "s3:RequestObjectTag/anotherparameter": "xxx" }
   }}}
        ENV:
        s3:RequestObjectTag/anotherparameter: xxx

Basically NotEquals/NotEqualsIfExists when 0 or 1 key is missing and the second match is false.
If my understanding is correct, the results should be the following:

DISCRIMINATOR 1: false (both values exist, one of them is equal and thus unmatched)
DISCRIMINATOR 2: true (only 1 value exists. It is unmatched, but the second IfExists makes ALL condition true)
Hm, I think I have found the discriminator: ```DISCRIMINATOR 1: { "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringNotEqualsIfExists": { "s3:RequestObjectTag/environment": "production" }, "StringNotEqualsIfExists": { "s3:RequestObjectTag/anotherparameter": "xxx" } }}} ENV: s3:RequestObjectTag/environment: development s3:RequestObjectTag/anotherparameter: xxx DISCRIMINATOR 2: { "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::bucket/*", "Condition": { "StringNotEqualsIfExists": { "s3:RequestObjectTag/environment": "production" }, "StringNotEqualsIfExists": { "s3:RequestObjectTag/anotherparameter": "xxx" } }}} ENV: s3:RequestObjectTag/anotherparameter: xxx ``` Basically NotEquals/NotEqualsIfExists when 0 or 1 key is missing and the second match is false. If my understanding is correct, the results should be the following: ``` DISCRIMINATOR 1: false (both values exist, one of them is equal and thus unmatched) DISCRIMINATOR 2: true (only 1 value exists. It is unmatched, but the second IfExists makes ALL condition true) ```

or no, my brain has melted

or no, my brain has melted

It seems no.
I have the following policy (StringNotEqualsIfExists can be added only once):

{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Principal": "*",
    "Action": "s3:PutObjectTagging",
    "Resource": [
        "arn:aws:s3:::yvo6fckpk92rhcsz",
        "arn:aws:s3:::yvo6fckpk92rhcsz/*"
    ],
    "Condition":{
        "StringNotEqualsIfExists": {
                "s3:RequestObjectTag/environment": "production" ,
                "s3:RequestObjectTag/anotherparameter": "xxx"
         }
    }
  }
}

And I get the following results:

$ aws s3api put-object-tagging --bucket yvo6fckpk92rhcsz --key tmp --no-sign-request --tagging '{"TagSet": [ { "Key": "anotherparameter", "Value": "xxx" } ]}'

An error occurred (AccessDenied) when calling the PutObjectTagging operation (reached max retries: 0): Access Denied



$ aws s3api put-object-tagging --bucket yvo6fckpk92rhcsz --key tmp --no-sign-request --tagging '{"TagSet": [ { "Key": "anotherparameter", "Value": "xxx2" } ]}'

{
    "VersionId": "null"
}



$ aws s3api put-object-tagging --bucket yvo6fckpk92rhcsz --key tmp --no-sign-request --tagging '{"TagSet": [ { "Key": "anotherparameter", "Value": "xxx2" }, {"Key": "environment", "Value": "production"} ]}'

An error occurred (AccessDenied) when calling the PutObjectTagging operation (reached max retries: 0): Access Denied



$ aws s3api put-object-tagging --bucket yvo6fckpk92rhcsz --key tmp --no-sign-request --tagging '{"TagSet": [  ]}'

{
    "VersionId": "null"
}
It seems no. I have the following policy (`StringNotEqualsIfExists` can be added only once): ```json { "Version": "2012-10-17", "Statement": { "Effect": "Allow", "Principal": "*", "Action": "s3:PutObjectTagging", "Resource": [ "arn:aws:s3:::yvo6fckpk92rhcsz", "arn:aws:s3:::yvo6fckpk92rhcsz/*" ], "Condition":{ "StringNotEqualsIfExists": { "s3:RequestObjectTag/environment": "production" , "s3:RequestObjectTag/anotherparameter": "xxx" } } } } ``` And I get the following results: ```shell $ aws s3api put-object-tagging --bucket yvo6fckpk92rhcsz --key tmp --no-sign-request --tagging '{"TagSet": [ { "Key": "anotherparameter", "Value": "xxx" } ]}' An error occurred (AccessDenied) when calling the PutObjectTagging operation (reached max retries: 0): Access Denied $ aws s3api put-object-tagging --bucket yvo6fckpk92rhcsz --key tmp --no-sign-request --tagging '{"TagSet": [ { "Key": "anotherparameter", "Value": "xxx2" } ]}' { "VersionId": "null" } $ aws s3api put-object-tagging --bucket yvo6fckpk92rhcsz --key tmp --no-sign-request --tagging '{"TagSet": [ { "Key": "anotherparameter", "Value": "xxx2" }, {"Key": "environment", "Value": "production"} ]}' An error occurred (AccessDenied) when calling the PutObjectTagging operation (reached max retries: 0): Access Denied $ aws s3api put-object-tagging --bucket yvo6fckpk92rhcsz --key tmp --no-sign-request --tagging '{"TagSet": [ ]}' { "VersionId": "null" } ```

So NotEquals doesn't differ from NotEqualsIfExists, right?

Currently, in this pull-request - it doesn't, but I believe that's why the doc clarifies:

If you are using an "Effect": "Deny" element with a negated condition operator like StringNotEqualsIfExists, the request is still denied even if the condition key is not present.

So, status should be propagated to Match function to check ...NotEqualsIfExists operators

> So `NotEquals` doesn't differ from `NotEqualsIfExists`, right? Currently, in this pull-request - it doesn't, but I believe that's why the [doc](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html#Conditions_IfExists) clarifies: > If you are using an "Effect": "Deny" element with a negated condition operator like StringNotEqualsIfExists, the request is still denied even if the condition key is not present. So, status should be propagated to `Match` function to check `...NotEqualsIfExists` operators

So, status should be propagated

Hm, so you have understood this sentence (I have not). Could you give an example of policy, where notequal and notequalifexists behave differently?

Anyway, let's not use negative ifexists in this PR.

>So, status should be propagated Hm, so you have understood this sentence (I have not). Could you give an example of policy, where notequal and notequalifexists behave differently? Anyway, let's not use negative ifexists in this PR.

let's not use negative ifexists in this PR

OK. I have already removed these operators and updated PR but let me check the tests provided by @dkirillov first

Could you give an example of policy, where notequal and notequalifexists behave differently?

I mean, I confirmed that you was right - these both operators, actually, had had no difference if key is present.

  • StringNotEquals returns false if key is not present (seems OK)
  • StringEqualsIfExists returns true if key is not present ("If the key is not present, evaluate the condition element as true.")
  • StringNotEqualsIfExists: returns true if key is not present. Okay, I was wrong about "propagating status" - it doesn't make sense. This mind-blowing sentence that confuses us declares the same behavior for ...Not...IfExists operators ("If the key is not present, evaluate the condition element as true."). Possibly, that's because negated could be mistaken for
StringNotEqualsIfExists(x, y) = ¬StringEqualsIfExists(x, y) = 
= ¬(¬Present(Properties, Key_X) OR Value(Key_X) == Y)) =
= Present(Properties, Key_X) && Value(Key_X) != Y = StringNotEquals(x,y)-> condition won't work out if key is not present

So, it must be interpreted only as (corresponding to the specification):

StringNotEqualsIfExists(x, y) = ¬Present(Properties, Key_X) || Value(Key_X) != Y

while

StringNotEquals(x,y) = Present(Properties, Key_X) && Value(Key_X) != Y

Anyway, let's keep these negated operators far away for the goodness sake

> let's not use negative ifexists in this PR OK. I have already removed these operators and updated PR but let me check the tests provided by @dkirillov first > Could you give an example of policy, where notequal and notequalifexists behave differently? I mean, I confirmed that you was right - these both operators, actually, had had no difference if key is present. - `StringNotEquals` returns `false` if key is not present (seems OK) - `StringEqualsIfExists` returns `true` if key is not present ("If the key is not present, evaluate the condition element as true.") - `StringNotEqualsIfExists`: returns `true` if key is not present. **Okay, I was wrong about "propagating status" - it doesn't make sense**. This mind-blowing sentence that confuses us declares the same behavior for `...Not...IfExists` operators ("If the key is not present, evaluate the condition element as true."). Possibly, that's because negated could be **mistaken** for ``` StringNotEqualsIfExists(x, y) = ¬StringEqualsIfExists(x, y) = = ¬(¬Present(Properties, Key_X) OR Value(Key_X) == Y)) = = Present(Properties, Key_X) && Value(Key_X) != Y = StringNotEquals(x,y)-> condition won't work out if key is not present ``` So, it must be interpreted only as (corresponding to the specification): ``` StringNotEqualsIfExists(x, y) = ¬Present(Properties, Key_X) || Value(Key_X) != Y ``` while ``` StringNotEquals(x,y) = Present(Properties, Key_X) && Value(Key_X) != Y ``` **Anyway**, let's keep these negated operators far away for the goodness sake
case CondStringNotLike: case CondStringNotLike:
return !util.GlobMatch(val, c.Value) return exists && !util.GlobMatch(val, c.Value)
case CondStringLessThan: case CondStringLessThan:
return val < c.Value return exists && val < c.Value
case CondStringLessThanIfExists:
return !exists || val < c.Value
case CondStringLessThanEquals: case CondStringLessThanEquals:
return val <= c.Value return exists && val <= c.Value
case CondStringLessThanEqualsIfExists:
return !exists || val <= c.Value
case CondStringGreaterThan: case CondStringGreaterThan:
return val > c.Value return exists && val > c.Value
case CondStringGreaterThanIfExists:
return !exists || val > c.Value
case CondStringGreaterThanEquals: case CondStringGreaterThanEquals:
return val >= c.Value return exists && val >= c.Value
case CondStringGreaterThanEqualsIfExists:
return !exists || val >= c.Value
case CondSliceContains: case CondSliceContains:
return slices.Contains(strings.Split(val, condSliceContainsDelimiter), c.Value) return slices.Contains(strings.Split(val, condSliceContainsDelimiter), c.Value)
case CondNumericEquals, CondNumericNotEquals, CondNumericLessThan, CondNumericLessThanEquals, CondNumericGreaterThan, case CondNumericEquals, CondNumericNotEquals, CondNumericLessThan, CondNumericLessThanEquals, CondNumericGreaterThan,
CondNumericGreaterThanEquals: CondNumericGreaterThanEquals:
return c.matchNumeric(val) return exists && c.matchNumeric(val)
case CondNumericEqualsIfExists, CondNumericLessThanIfExists, CondNumericLessThanEqualsIfExists, CondNumericGreaterThanIfExists,
CondNumericGreaterThanEqualsIfExists:
return !exists || c.matchNumeric(val)
case CondIPAddress, CondNotIPAddress: case CondIPAddress, CondNotIPAddress:
return c.matchIP(val) return c.matchIP(val)
} }
@ -213,17 +257,17 @@ func (c *Condition) matchNumeric(val string) bool {
switch c.Op { switch c.Op {
default: default:
panic(fmt.Sprintf("unimplemented: %d", c.Op)) panic(fmt.Sprintf("unimplemented: %d", c.Op))
case CondNumericEquals: case CondNumericEquals, CondNumericEqualsIfExists:
return valDecimal.Equal(condVal) return valDecimal.Equal(condVal)
case CondNumericNotEquals: case CondNumericNotEquals:
return !valDecimal.Equal(condVal) return !valDecimal.Equal(condVal)
case CondNumericLessThan: case CondNumericLessThan, CondNumericLessThanIfExists:
return valDecimal.LessThan(condVal) return valDecimal.LessThan(condVal)
case CondNumericLessThanEquals: case CondNumericLessThanEquals, CondNumericLessThanEqualsIfExists:
return valDecimal.LessThan(condVal) || valDecimal.Equal(condVal) return valDecimal.LessThan(condVal) || valDecimal.Equal(condVal)
case CondNumericGreaterThan: case CondNumericGreaterThan, CondNumericGreaterThanIfExists:
return valDecimal.GreaterThan(condVal) return valDecimal.GreaterThan(condVal)
case CondNumericGreaterThanEquals: case CondNumericGreaterThanEquals, CondNumericGreaterThanEqualsIfExists:
return valDecimal.GreaterThan(condVal) || valDecimal.Equal(condVal) return valDecimal.GreaterThan(condVal) || valDecimal.Equal(condVal)
} }
} }

View file

@ -398,6 +398,25 @@ func testNumericConditionsMatch(t *testing.T) {
value: "50", value: "50",
status: Allow, status: Allow,
}, },
{
name: "value if exists from interval",
conditions: []Condition{
{
Op: CondNumericLessThanIfExists,
Kind: KindRequest,
Key: propKey,
Value: "100",
},
{
Op: CondNumericGreaterThanIfExists,
Kind: KindRequest,
Key: propKey,
Value: "80",
},
},
value: "90",
status: Allow,
},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
resource := testutil.NewResource(native.ResourceFormatRootContainers, nil) resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
@ -411,6 +430,16 @@ func testNumericConditionsMatch(t *testing.T) {
}}} }}}
st, _ := ch.Match(request) st, _ := ch.Match(request)
require.Equal(t, tc.status.String(), st.String()) require.Equal(t, tc.status.String(), st.String())
emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{})
st, found := ch.Match(emptyPropsRequest)
if strings.HasSuffix(tc.conditions[0].Op.String(), "IfExists") {
require.True(t, found)
require.Equal(t, tc.status.String(), st.String())
} else {
require.False(t, found)
require.Equal(t, st, NoRuleFound)
}
}) })
} }
} }
@ -448,6 +477,47 @@ func testStringConiditionsMatch(t *testing.T) {
st, found = ch.Match(request) st, found = ch.Match(request)
require.False(t, found) require.False(t, found)
require.Equal(t, NoRuleFound, st) require.Equal(t, NoRuleFound, st)
emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{})
_, found = ch.Match(emptyPropsRequest)
require.False(t, found)
require.Equal(t, NoRuleFound, st)
})
t.Run(CondStringEqualsIfExists.String(), func(t *testing.T) {
ch := Chain{Rules: []Rule{{
Status: Allow,
Actions: Actions{Names: []string{native.MethodPutObject}},
Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
Condition: []Condition{{
Op: CondStringEqualsIfExists,
Kind: KindRequest,
Key: propKey,
Value: val,
}},
}}}
resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
propKey: val,
})
st, found := ch.Match(request)
require.True(t, found)
require.Equal(t, Allow, st)
request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
propKey: "distort_tag_value" + val,
})
st, found = ch.Match(request)
require.False(t, found)
require.Equal(t, NoRuleFound, st)
emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{})
st, found = ch.Match(emptyPropsRequest)
require.True(t, found)
require.Equal(t, Allow, st)
}) })
t.Run(CondStringNotEquals.String(), func(t *testing.T) { t.Run(CondStringNotEquals.String(), func(t *testing.T) {
@ -479,6 +549,11 @@ func testStringConiditionsMatch(t *testing.T) {
st, found = ch.Match(request) st, found = ch.Match(request)
require.False(t, found) require.False(t, found)
require.Equal(t, NoRuleFound, st) require.Equal(t, NoRuleFound, st)
emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{})
_, found = ch.Match(emptyPropsRequest)
require.False(t, found)
require.Equal(t, NoRuleFound, st)
}) })
t.Run(CondStringEqualsIgnoreCase.String(), func(t *testing.T) { t.Run(CondStringEqualsIgnoreCase.String(), func(t *testing.T) {
@ -510,6 +585,47 @@ func testStringConiditionsMatch(t *testing.T) {
st, found = ch.Match(request) st, found = ch.Match(request)
require.False(t, found) require.False(t, found)
require.Equal(t, NoRuleFound, st) require.Equal(t, NoRuleFound, st)
emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{})
_, found = ch.Match(emptyPropsRequest)
require.False(t, found)
require.Equal(t, NoRuleFound, st)
})
t.Run(CondStringEqualsIgnoreCaseIfExists.String(), func(t *testing.T) {
ch := Chain{Rules: []Rule{{
Status: Allow,
Actions: Actions{Names: []string{native.MethodPutObject}},
Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
Condition: []Condition{{
Op: CondStringEqualsIgnoreCaseIfExists,
Kind: KindRequest,
Key: propKey,
Value: val,
}},
}}}
resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
propKey: strings.ToUpper(val),
})
st, found := ch.Match(request)
require.True(t, found)
require.Equal(t, Allow, st)
request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
propKey: strings.ToUpper("distort_tag_value" + val),
})
st, found = ch.Match(request)
require.False(t, found)
require.Equal(t, NoRuleFound, st)
emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{})
st, found = ch.Match(emptyPropsRequest)
require.True(t, found)
require.Equal(t, Allow, st)
}) })
t.Run(CondStringNotEqualsIgnoreCase.String(), func(t *testing.T) { t.Run(CondStringNotEqualsIgnoreCase.String(), func(t *testing.T) {
@ -541,6 +657,11 @@ func testStringConiditionsMatch(t *testing.T) {
st, found = ch.Match(request) st, found = ch.Match(request)
require.False(t, found) require.False(t, found)
require.Equal(t, NoRuleFound, st) require.Equal(t, NoRuleFound, st)
emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{})
st, found = ch.Match(emptyPropsRequest)
require.False(t, found)
require.Equal(t, NoRuleFound, st)
}) })
t.Run(CondStringLike.String(), func(t *testing.T) { t.Run(CondStringLike.String(), func(t *testing.T) {
@ -572,6 +693,47 @@ func testStringConiditionsMatch(t *testing.T) {
st, found = ch.Match(request) st, found = ch.Match(request)
require.False(t, found) require.False(t, found)
require.Equal(t, NoRuleFound, st) require.Equal(t, NoRuleFound, st)
emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{})
st, found = ch.Match(emptyPropsRequest)
require.False(t, found)
require.Equal(t, NoRuleFound, st)
})
t.Run(CondStringLikeIfExists.String(), func(t *testing.T) {
ch := Chain{Rules: []Rule{{
Status: Allow,
Actions: Actions{Names: []string{native.MethodPutObject}},
Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
Condition: []Condition{{
Op: CondStringLikeIfExists,
Kind: KindRequest,
Key: propKey,
Value: val + "*",
}},
}}}
resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
propKey: val + "suffix",
})
st, found := ch.Match(request)
require.True(t, found)
require.Equal(t, Allow, st)
request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
propKey: string([]byte(val)[:len(val)-1]), //cut last letter
})
st, found = ch.Match(request)
require.False(t, found)
require.Equal(t, NoRuleFound, st)
emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{})
st, found = ch.Match(emptyPropsRequest)
require.True(t, found)
require.Equal(t, Allow, st)
}) })
t.Run(CondStringNotLike.String(), func(t *testing.T) { t.Run(CondStringNotLike.String(), func(t *testing.T) {
@ -603,6 +765,11 @@ func testStringConiditionsMatch(t *testing.T) {
st, found = ch.Match(request) st, found = ch.Match(request)
require.False(t, found) require.False(t, found)
require.Equal(t, NoRuleFound, st) require.Equal(t, NoRuleFound, st)
emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{})
st, found = ch.Match(emptyPropsRequest)
require.False(t, found)
require.Equal(t, NoRuleFound, st)
}) })
t.Run(CondStringLessThan.String(), func(t *testing.T) { t.Run(CondStringLessThan.String(), func(t *testing.T) {
@ -634,6 +801,47 @@ func testStringConiditionsMatch(t *testing.T) {
st, found = ch.Match(request) st, found = ch.Match(request)
require.False(t, found) require.False(t, found)
require.Equal(t, NoRuleFound, st) require.Equal(t, NoRuleFound, st)
emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{})
st, found = ch.Match(emptyPropsRequest)
require.False(t, found)
require.Equal(t, NoRuleFound, st)
})
t.Run(CondStringLessThanIfExists.String(), func(t *testing.T) {
ch := Chain{Rules: []Rule{{
Status: Allow,
Actions: Actions{Names: []string{native.MethodPutObject}},
Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
Condition: []Condition{{
Op: CondStringLessThanIfExists,
Kind: KindRequest,
Key: propKey,
Value: val + "b",
}},
}}}
resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
propKey: val + "a",
})
st, found := ch.Match(request)
require.True(t, found)
require.Equal(t, Allow, st)
request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
propKey: val + "c",
})
st, found = ch.Match(request)
require.False(t, found)
require.Equal(t, NoRuleFound, st)
emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{})
st, found = ch.Match(emptyPropsRequest)
require.True(t, found)
require.Equal(t, Allow, st)
}) })
t.Run(CondStringLessThanEquals.String(), func(t *testing.T) { t.Run(CondStringLessThanEquals.String(), func(t *testing.T) {
@ -665,6 +873,47 @@ func testStringConiditionsMatch(t *testing.T) {
st, found = ch.Match(request) st, found = ch.Match(request)
require.True(t, found) require.True(t, found)
require.Equal(t, Allow, st) require.Equal(t, Allow, st)
emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{})
st, found = ch.Match(emptyPropsRequest)
require.False(t, found)
require.Equal(t, NoRuleFound, st)
})
t.Run(CondStringLessThanEqualsIfExists.String(), func(t *testing.T) {
ch := Chain{Rules: []Rule{{
Status: Allow,
Actions: Actions{Names: []string{native.MethodPutObject}},
Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
Condition: []Condition{{
Op: CondStringLessThanEqualsIfExists,
Kind: KindRequest,
Key: propKey,
Value: val + "b",
}},
}}}
resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
propKey: val + "a",
})
st, found := ch.Match(request)
require.True(t, found)
require.Equal(t, Allow, st)
request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
propKey: val + "b",
})
st, found = ch.Match(request)
require.True(t, found)
require.Equal(t, Allow, st)
emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{})
st, found = ch.Match(emptyPropsRequest)
require.True(t, found)
require.Equal(t, Allow, st)
}) })
t.Run(CondStringGreaterThan.String(), func(t *testing.T) { t.Run(CondStringGreaterThan.String(), func(t *testing.T) {
@ -696,6 +945,47 @@ func testStringConiditionsMatch(t *testing.T) {
st, found = ch.Match(request) st, found = ch.Match(request)
require.False(t, found) require.False(t, found)
require.Equal(t, NoRuleFound, st) require.Equal(t, NoRuleFound, st)
emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{})
st, found = ch.Match(emptyPropsRequest)
require.False(t, found)
require.Equal(t, NoRuleFound, st)
})
t.Run(CondStringGreaterThanIfExists.String(), func(t *testing.T) {
ch := Chain{Rules: []Rule{{
Status: Allow,
Actions: Actions{Names: []string{native.MethodPutObject}},
Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
Condition: []Condition{{
Op: CondStringGreaterThanIfExists,
Kind: KindRequest,
Key: propKey,
Value: val + "b",
}},
}}}
resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
propKey: val + "c",
})
st, found := ch.Match(request)
require.True(t, found)
require.Equal(t, Allow, st)
request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
propKey: val + "b",
})
st, found = ch.Match(request)
require.False(t, found)
require.Equal(t, NoRuleFound, st)
emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{})
st, found = ch.Match(emptyPropsRequest)
require.True(t, found)
require.Equal(t, Allow, st)
}) })
t.Run(CondStringGreaterThanEquals.String(), func(t *testing.T) { t.Run(CondStringGreaterThanEquals.String(), func(t *testing.T) {
@ -727,6 +1017,47 @@ func testStringConiditionsMatch(t *testing.T) {
st, found = ch.Match(request) st, found = ch.Match(request)
require.True(t, found) require.True(t, found)
require.Equal(t, Allow, st) require.Equal(t, Allow, st)
emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{})
st, found = ch.Match(emptyPropsRequest)
require.False(t, found)
require.Equal(t, NoRuleFound, st)
})
t.Run(CondStringGreaterThanEqualsIfExists.String(), func(t *testing.T) {
ch := Chain{Rules: []Rule{{
Status: Allow,
Actions: Actions{Names: []string{native.MethodPutObject}},
Resources: Resources{Names: []string{native.ResourceFormatRootContainers}},
Condition: []Condition{{
Op: CondStringGreaterThanEqualsIfExists,
Kind: KindRequest,
Key: propKey,
Value: val + "b",
}},
}}}
resource := testutil.NewResource(native.ResourceFormatRootContainers, nil)
request := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
propKey: val + "c",
})
st, found := ch.Match(request)
require.True(t, found)
require.Equal(t, Allow, st)
request = testutil.NewRequest(native.MethodPutObject, resource, map[string]string{
propKey: val + "b",
})
st, found = ch.Match(request)
require.True(t, found)
require.Equal(t, Allow, st)
emptyPropsRequest := testutil.NewRequest(native.MethodPutObject, resource, map[string]string{})
st, found = ch.Match(emptyPropsRequest)
require.True(t, found)
require.Equal(t, Allow, st)
}) })
} }

View file

@ -7,13 +7,17 @@ type Request interface {
// Name is the operation name, such as Object.Put. Must not include wildcards. // Name is the operation name, such as Object.Put. Must not include wildcards.
Operation() string Operation() string
// Property returns request properties, such as IP address of the origin. // Property returns request properties, such as IP address of the origin.
Property(string) string // The second return boolean value determines if the specified value exists within the properties.
Property(string) (string, bool)
// Resource returns resource the operation is applied to. // Resource returns resource the operation is applied to.
Resource() Resource Resource() Resource
} }
// Resource represents the resource operation is applied to. // Resource represents the resource operation is applied to.
type Resource interface { type Resource interface {
// Name is the resource name.
Name() string Name() string
Property(string) string // Property returns resource properties, such as object type etc.
// The second return boolean value determines if the specified value exists within the properties.
Property(string) (string, bool)
} }

View file

@ -13,8 +13,9 @@ func (r *Resource) Name() string {
return r.name return r.name
} }
func (r *Resource) Property(name string) string { func (r *Resource) Property(name string) (val string, exists bool) {
return r.properties[name] val, exists = r.properties[name]
dkirillov marked this conversation as resolved Outdated

Why don't we write just return r.properties[name] ?

Why don't we write just `return r.properties[name]` ?

Because compiler doesn't allow this:

not enough return values
	have (string)
	want (string, bool)
Because compiler doesn't allow this: ``` not enough return values have (string) want (string, bool) ```
return
} }
func NewResource(name string, properties map[string]string) *Resource { func NewResource(name string, properties map[string]string) *Resource {
@ -40,8 +41,9 @@ func (r *Request) Resource() resourcepkg.Resource {
return r.resource return r.resource
} }
func (r *Request) Property(name string) string { func (r *Request) Property(name string) (val string, exists bool) {
return r.properties[name] val, exists = r.properties[name]
return
} }
func NewRequest(op string, r *Resource, properties map[string]string) *Request { func NewRequest(op string, r *Resource, properties map[string]string) *Request {