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
27 changes: 25 additions & 2 deletions policy/condition/keyname.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,22 @@ var toTrim = map[string]bool{
adminKeyPrefix: true,
}

// untrimmedKeys read the request value named by their whole key, prefix
// included, because trimming would collide with a key that carries a
// different value. Within an STS call aws:SourceIdentity is the calling
// session's source identity and sts:SourceIdentity the one the new session
// will carry.
var untrimmedKeys = map[KeyName]bool{
STSSourceIdentity: true,
}

// Name - returns the key name with its service prefix stripped, so a key reads
// the request value of the same name. The prefixes that are stripped are the
// keys of toTrim; a service missing from there keeps its whole name and reads a
// value nothing populates.
// value nothing populates. A key in untrimmedKeys keeps its whole name too.
func (key KeyName) Name() string {
idx := strings.IndexByte(string(key), ':')
if idx == -1 || !toTrim[string(key[:idx])] {
if idx == -1 || !toTrim[string(key[:idx])] || untrimmedKeys[key] {
return string(key)
}
return string(key[idx+1:])
Expand Down Expand Up @@ -240,6 +249,10 @@ const (
// AWSGroups - groups for any authenticating Access Key.
AWSGroups KeyName = "aws:groups"

// AWSSourceIdentity - the source identity of the session that signed the
// request, set when the session was assumed and fixed for its lifetime.
AWSSourceIdentity KeyName = "aws:SourceIdentity"

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

Expand Down Expand Up @@ -308,6 +321,10 @@ const (
// STSDurationSeconds - Duration seconds condition for STS policy
STSDurationSeconds KeyName = "sts:DurationSeconds"

// STSSourceIdentity - the source identity the session being assumed will
// carry: the one requested, or the one inherited from the caller's session.
STSSourceIdentity KeyName = "sts:SourceIdentity"

// SVCDurationSeconds - Duration seconds condition for Admin policy
SVCDurationSeconds KeyName = "svc:DurationSeconds"

Expand Down Expand Up @@ -389,6 +406,7 @@ var AllSupportedKeys = []KeyName{
AWSUserID,
AWSUsername,
AWSGroups,
AWSSourceIdentity,
LDAPUser,
LDAPUsername,
LDAPGroups,
Expand Down Expand Up @@ -418,6 +436,7 @@ var AllSupportedKeys = []KeyName{
JWTScope,
JWTClientID,
STSDurationSeconds,
STSSourceIdentity,
SVCDurationSeconds,
AdminPolicyName,
}
Expand All @@ -440,6 +459,7 @@ var CommonKeys = append([]KeyName{
AWSUserID,
AWSUsername,
AWSGroups,
AWSSourceIdentity,
LDAPUser,
LDAPUsername,
LDAPGroups,
Expand Down Expand Up @@ -468,6 +488,7 @@ var CommonAdminKeys = append([]KeyName{
AWSUserID,
AWSUsername,
AWSGroups,
AWSSourceIdentity,
LDAPUser,
LDAPUsername,
LDAPGroups,
Expand All @@ -491,6 +512,8 @@ var AllSupportedSTSKeys = []KeyName{
AWSPrincipalType,
AWSSecureTransport,
STSDurationSeconds,
STSSourceIdentity,
AWSSourceIdentity,
LDAPUser,
AWSUserID,
AWSGroups,
Expand Down
4 changes: 2 additions & 2 deletions policy/condition/keyname_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ func TestServicePrefixedKeysResolveToRequestNames(t *testing.T) {
for _, key := range AllSupportedKeys {
full := string(key)
idx := strings.IndexByte(full, ':')
if idx < 0 {
// Unprefixed keys read their own name.
if idx < 0 || untrimmedKeys[key] {
// Unprefixed and untrimmed keys read their own name.
if got := key.Name(); got != full {
t.Errorf("%s.Name() = %q, want %q", key, got, full)
}
Expand Down
177 changes: 177 additions & 0 deletions policy/source_identity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
// 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"
)

// TestSourceIdentityConditions verifies that policies can grant S3, admin and
// STS actions on a source identity: aws:SourceIdentity for the signing
// session's, sts:SourceIdentity for the one an STS call's new session will
// carry.
func TestSourceIdentityConditions(t *testing.T) {
doc := `{
"Version": "2012-10-17",
"Statement": [
{"Effect": "Allow", "Action": ["s3:GetObject"], "Resource": ["arn:aws:s3:::data/*"],
"Condition": {"StringLike": {"aws:SourceIdentity": ["alice*"]}}},
{"Effect": "Allow", "Action": ["admin:ServerInfo"],
"Condition": {"StringEquals": {"aws:SourceIdentity": ["alice"]}}},
{"Effect": "Allow", "Action": ["sts:AssumeRole"],
"Condition": {"StringEquals": {"sts:SourceIdentity": ["alice"]}}}
Comment thread
harshavardhana marked this conversation as resolved.
]
}`
p, err := ParseConfig(strings.NewReader(doc))
if err != nil {
t.Fatalf("a policy naming the source identity keys must parse: %v", err)
}

cases := []struct {
action Action
sourceIdentity []string
allowed bool
}{
{GetObjectAction, []string{"alice@example.com"}, true},
{GetObjectAction, []string{"bob"}, false},
{GetObjectAction, nil, false},
{Action(ServerInfoAdminAction), []string{"alice"}, true},
{Action(ServerInfoAdminAction), []string{"bob"}, false},
{Action(AssumeRoleAction), []string{"alice"}, true},
{Action(AssumeRoleAction), []string{"bob"}, false},
{Action(AssumeRoleAction), nil, false},
}
for _, tc := range cases {
Comment thread
harshavardhana marked this conversation as resolved.
values := map[string][]string{}
if tc.sourceIdentity != nil {
values[condition.AWSSourceIdentity.Name()] = tc.sourceIdentity
values[condition.STSSourceIdentity.Name()] = tc.sourceIdentity
}
got := p.IsAllowed(Args{
AccountName: "app",
Action: tc.action,
BucketName: "data",
ObjectName: "report.csv",
ConditionValues: values,
})
if got != tc.allowed {
t.Errorf("%s with source identity %v: allowed=%v, want %v", tc.action, tc.sourceIdentity, got, tc.allowed)
}
}
}

// TestSourceIdentityConditionsDeny verifies that a Deny keyed on a source
// identity overrides a broader Allow, and that a StringNotEquals Deny refuses a
// request that carries no source identity.
func TestSourceIdentityConditionsDeny(t *testing.T) {
doc := `{
"Version": "2012-10-17",
"Statement": [
{"Effect": "Allow", "Action": ["s3:GetObject"], "Resource": ["arn:aws:s3:::data/*"]},
{"Effect": "Allow", "Action": ["sts:AssumeRole"]},
{"Effect": "Deny", "Action": ["s3:GetObject"], "Resource": ["arn:aws:s3:::data/*"],
"Condition": {"StringEquals": {"aws:SourceIdentity": ["mallory"]}}},
{"Effect": "Deny", "Action": ["sts:AssumeRole"],
"Condition": {"StringNotEquals": {"sts:SourceIdentity": ["alice"]}}}
]
}`
p, err := ParseConfig(strings.NewReader(doc))
if err != nil {
t.Fatalf("a policy denying on the source identity keys must parse: %v", err)
}

cases := []struct {
action Action
sourceIdentity []string
allowed bool
}{
{GetObjectAction, []string{"alice"}, true},
{GetObjectAction, []string{"mallory"}, false},
{GetObjectAction, nil, true},
{Action(AssumeRoleAction), []string{"alice"}, true},
{Action(AssumeRoleAction), []string{"bob"}, false},
{Action(AssumeRoleAction), nil, false},
}
for _, tc := range cases {
values := map[string][]string{}
if tc.sourceIdentity != nil {
values[condition.AWSSourceIdentity.Name()] = tc.sourceIdentity
values[condition.STSSourceIdentity.Name()] = tc.sourceIdentity
}
got := p.IsAllowed(Args{
AccountName: "app",
Action: tc.action,
BucketName: "data",
ObjectName: "report.csv",
ConditionValues: values,
})
if got != tc.allowed {
t.Errorf("%s with source identity %v: allowed=%v, want %v", tc.action, tc.sourceIdentity, got, tc.allowed)
}
}
}

// TestSourceIdentityKeysReadSeparateValues verifies that within one STS call
// aws:SourceIdentity reads the calling session's source identity and
// sts:SourceIdentity the one the new session will carry, so a policy can tell a
// session setting its source identity for the first time from one inheriting
// it.
func TestSourceIdentityKeysReadSeparateValues(t *testing.T) {
doc := `{
"Version": "2012-10-17",
"Statement": [
{"Effect": "Allow", "Action": ["sts:AssumeRole"],
"Condition": {"StringEquals": {"sts:SourceIdentity": ["alice"]}}},
{"Effect": "Deny", "Action": ["sts:AssumeRole"],
"Condition": {"Null": {"aws:SourceIdentity": ["true"]}}}
]
}`
p, err := ParseConfig(strings.NewReader(doc))
if err != nil {
t.Fatal(err)
}

cases := []struct {
name string
caller, session string
allowed bool
}{
{name: "caller carries alice, session inherits it", caller: "alice", session: "alice", allowed: true},
{name: "caller carries none, session sets alice", session: "alice"},
{name: "caller carries bob, session inherits it", caller: "bob", session: "bob"},
}
for _, tc := range cases {
values := map[string][]string{}
if tc.caller != "" {
values[condition.AWSSourceIdentity.Name()] = []string{tc.caller}
}
if tc.session != "" {
values[condition.STSSourceIdentity.Name()] = []string{tc.session}
}
got := p.IsAllowed(Args{AccountName: "app", Action: Action(AssumeRoleAction), ConditionValues: values})
if got != tc.allowed {
t.Errorf("%s: allowed=%v, want %v", tc.name, got, tc.allowed)
}
}
if condition.AWSSourceIdentity.Name() == condition.STSSourceIdentity.Name() {
t.Errorf("aws:SourceIdentity and sts:SourceIdentity both read %q", condition.AWSSourceIdentity.Name())
}
}
Loading