Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions policy/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,9 @@ func (a ActionConditionKeyMap) Lookup(action Action) condition.KeySet {
}

ckeysMerged := condition.NewKeySet(commonKeys...)
if !bucketlessActions[action] {
ckeysMerged.Merge(bucketTagKeys)
}
for act, ckey := range a {
if action.Match(act) {
ckeysMerged.Merge(ckey)
Expand All @@ -406,6 +409,19 @@ func (a ActionConditionKeyMap) Lookup(action Action) condition.KeySet {
return ckeysMerged
}

// bucketTagKeys are the keys every S3 action that acts on an existing bucket
// takes, read from that bucket's tags.
var bucketTagKeys = condition.NewKeySet(
condition.AWSResourceTag.ToKey(),
condition.S3BucketTag.ToKey(),
)

// bucketlessActions act on no existing bucket, so no bucket tags reach them.
var bucketlessActions = map[Action]bool{
ListAllMyBucketsAction: true,
CreateBucketAction: true,
}

// IAMActionConditionKeyMap - holds mapping of supported condition key for an action.
var IAMActionConditionKeyMap = createActionConditionKeyMap()

Expand Down Expand Up @@ -553,6 +569,8 @@ func createActionConditionKeyMap() ActionConditionKeyMap {
append([]condition.Key{
condition.RequestObjectTagKeys.ToKey(),
condition.RequestObjectTag.ToKey(),
condition.AWSRequestTag.ToKey(),
condition.AWSTagKeys.ToKey(),
}, commonKeys...)...),
PutObjectTaggingAction: condition.NewKeySet(
append([]condition.Key{
Expand Down
113 changes: 113 additions & 0 deletions policy/bucket_tag_condition_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
// Copyright (c) 2015-2026 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package policy

import (
"strings"
"testing"

"github.com/minio/pkg/v3/policy/condition"
)

// TestBucketTagConditions verifies that aws:ResourceTag/<key> and
// s3:BucketTag/<key> condition bucket and object actions on the tags of the
// bucket, and that aws:RequestTag/<key> and aws:TagKeys condition a bucket tag
// write on the tags it sets.
func TestBucketTagConditions(t *testing.T) {
doc := `{"Version":"2012-10-17","Statement":[
{"Effect":"Allow","Action":["s3:*"],"Resource":["arn:aws:s3:::*"]},
{"Effect":"Deny","Action":["s3:GetObject","s3:ListBucket"],"Resource":["arn:aws:s3:::*"],
"Condition":{"StringEquals":{"aws:ResourceTag/level":["restricted"]}}},
{"Effect":"Deny","Action":["s3:PutObject"],"Resource":["arn:aws:s3:::*"],
"Condition":{"StringEquals":{"s3:BucketTag/state":["frozen"]}}},
{"Effect":"Deny","Action":["s3:PutBucketTagging"],"Resource":["arn:aws:s3:::*"],
"Condition":{"ForAnyValue:StringLike":{"aws:TagKeys":["fabric.governance.*"]}}},
{"Effect":"Deny","Action":["s3:PutBucketTagging"],"Resource":["arn:aws:s3:::*"],
"Condition":{"StringEquals":{"aws:RequestTag/tier":["gold"]}}}
]}`
p, err := ParseConfig(strings.NewReader(doc))
if err != nil {
t.Fatal(err)
}
if err := p.CheckVariables(); err != nil {
t.Fatal(err)
}

args := func(action Action, object string, bucketTags, requestTags map[string]string) Args {
values := map[string][]string{}
for k, v := range bucketTags {
values[condition.NewKey(condition.AWSResourceTag, k).Name()] = []string{v}
values[condition.NewKey(condition.S3BucketTag, k).Name()] = []string{v}
}
var keys []string
for k, v := range requestTags {
keys = append(keys, k)
values[condition.NewKey(condition.AWSRequestTag, k).Name()] = []string{v}
}
if requestTags != nil {
values[condition.AWSTagKeys.Name()] = keys
}
return Args{AccountName: "user", Action: action, BucketName: "bucket", ObjectName: object, ConditionValues: values}
}
restricted := map[string]string{"level": "restricted"}

for _, tc := range []struct {
name string
args Args
want bool
}{
{"read in a restricted bucket", args(GetObjectAction, "obj", restricted, nil), false},
{"list a restricted bucket", args(ListBucketAction, "", restricted, nil), false},
{"read in a public bucket", args(GetObjectAction, "obj", map[string]string{"level": "public"}, nil), true},
{"read in an untagged bucket", args(GetObjectAction, "obj", nil, nil), true},
{"write in a restricted bucket", args(PutObjectAction, "obj", restricted, nil), true},
{"write in a frozen bucket", args(PutObjectAction, "obj", map[string]string{"state": "frozen"}, nil), false},
{"tag a bucket with a free key", args(PutBucketTaggingAction, "", nil, map[string]string{"team": "x"}), true},
{"tag a bucket with a governed key", args(PutBucketTaggingAction, "", nil, map[string]string{"team": "x", "fabric.governance.level": "public"}), false},
{"remove every bucket tag", args(PutBucketTaggingAction, "", nil, map[string]string{}), true},
{"tag a bucket with a denied value", args(PutBucketTaggingAction, "", nil, map[string]string{"tier": "gold"}), false},
} {
if got := p.IsAllowed(tc.args); got != tc.want {
t.Errorf("%s: IsAllowed = %v, want %v", tc.name, got, tc.want)
}
}

for _, accepted := range []string{
`{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":["s3:GetObject"],"Resource":["arn:aws:s3:::bucket/*"],"Condition":{"StringEquals":{"aws:ResourceTag/level":["restricted"]}}}]}`,
`{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":["s3:DeleteBucket"],"Resource":["arn:aws:s3:::bucket"],"Condition":{"StringEquals":{"s3:BucketTag/level":["restricted"]}}}]}`,
} {
if _, err := ParseConfig(strings.NewReader(accepted)); err != nil {
t.Errorf("a bucket tag key on an S3 action must be accepted: %v: %s", err, accepted)
}
if _, err := ParseBucketPolicyConfig(strings.NewReader(strings.Replace(accepted, `"Action"`, `"Principal":"*","Action"`, 1)), "bucket"); err != nil {
t.Errorf("a bucket tag key in a bucket policy must be accepted: %v: %s", err, accepted)
}
}

for _, refused := range []string{
`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:GetObject"],"Resource":["arn:aws:s3:::bucket/*"],"Condition":{"ForAnyValue:StringEquals":{"aws:TagKeys":["x"]}}}]}`,
`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:GetObject"],"Resource":["arn:aws:s3:::bucket/*"],"Condition":{"StringEquals":{"aws:RequestTag/x":["y"]}}}]}`,
`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3tables:GetTable"],"Resource":["arn:aws:s3tables:::bucket/*"],"Condition":{"StringEquals":{"aws:ResourceTag/x":["y"]}}}]}`,
`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:ListAllMyBuckets"],"Resource":["arn:aws:s3:::*"],"Condition":{"StringEquals":{"aws:ResourceTag/x":["y"]}}}]}`,
`{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["s3:CreateBucket"],"Resource":["arn:aws:s3:::bucket"],"Condition":{"StringEquals":{"s3:BucketTag/x":["y"]}}}]}`,
} {
if _, err := ParseConfig(strings.NewReader(refused)); err == nil {
t.Errorf("must be refused: %s", refused)
}
}
}
13 changes: 13 additions & 0 deletions policy/condition/keyname.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ var variableKeys = map[KeyName]bool{
S3TablesWarehouseTag: true,
S3TablesTableTag: true,
AWSRequestTag: true,
AWSResourceTag: true,
S3BucketTag: true,
}

func variableKeyNames() []string {
Expand Down Expand Up @@ -261,6 +263,15 @@ const (
// AWSTagKeys filters a tagging request by the tag keys it sets or removes.
AWSTagKeys KeyName = "aws:TagKeys"

// AWSResourceTag filters access by the value of one tag on the bucket the
// request acts on, or holds the object it acts on, as in
// aws:ResourceTag/<key>.
AWSResourceTag KeyName = "aws:ResourceTag"

// S3BucketTag filters access by the value of one tag on the bucket the
// request is authorized against, as in s3:BucketTag/<key>.
S3BucketTag KeyName = "s3:BucketTag"

// S3SignatureVersion - identifies the version of AWS Signature that you want to support for authenticated requests.
S3SignatureVersion KeyName = "s3:signatureversion"

Expand Down Expand Up @@ -417,6 +428,8 @@ var AllSupportedKeys = []KeyName{
AWSSourceIdentity,
AWSRequestTag,
AWSTagKeys,
AWSResourceTag,
S3BucketTag,
LDAPUser,
LDAPUsername,
LDAPGroups,
Expand Down
3 changes: 3 additions & 0 deletions policy/condition_variable_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ func TestConditionVariableAllowedOnTagKeys(t *testing.T) {
{"s3tables:TableTag/team", `"s3tables:GetTable"`, `"arn:aws:s3tables:::bucket/*"`},
{"s3tables:WarehouseTag/team", `"s3tables:GetTable"`, `"arn:aws:s3tables:::bucket/*"`},
{"aws:RequestTag/team", `"s3tables:TagTable"`, `"arn:aws:s3tables:::bucket/*"`},
{"aws:ResourceTag/team", `"s3:GetObject"`, `"arn:aws:s3:::bucket/*"`},
{"s3:BucketTag/team", `"s3:ListBucket"`, `"arn:aws:s3:::bucket"`},
{"aws:RequestTag/team", `"s3:PutBucketTagging"`, `"arn:aws:s3:::bucket"`},
} {
doc := `{"Version": "2012-10-17", "Statement": [` + conditionStatement(tc.actions, tc.resources, tc.key) + `]}`
p, err := ParseConfig(strings.NewReader(doc))
Expand Down
Loading