s3api: add IAM policy fallback authorization tests (#8518)
* s3api: add IAM policy fallback auth with tests * s3api: use policy engine for IAM fallback evaluation
This commit is contained in:
@@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
||||||
|
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/util/wildcard"
|
"github.com/seaweedfs/seaweedfs/weed/util/wildcard"
|
||||||
@@ -1658,6 +1659,52 @@ func determineIAMAuthPath(sessionToken, principal, principalArn string) iamAuthP
|
|||||||
return iamAuthPathNone
|
return iamAuthPathNone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// evaluateIAMPolicies evaluates attached IAM policies for a user identity.
|
||||||
|
// Returns true if any matching statement explicitly allows the action.
|
||||||
|
func (iam *IdentityAccessManagement) evaluateIAMPolicies(r *http.Request, identity *Identity, action Action, bucket, object string) bool {
|
||||||
|
if identity == nil || len(identity.PolicyNames) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
resource := buildResourceARN(bucket, object)
|
||||||
|
principal := buildPrincipalARN(identity, r)
|
||||||
|
s3Action := ResolveS3Action(r, string(action), bucket, object)
|
||||||
|
explicitAllow := false
|
||||||
|
conditions := policy_engine.ExtractConditionValuesFromRequest(r)
|
||||||
|
for k, v := range policy_engine.ExtractPrincipalVariables(principal) {
|
||||||
|
conditions[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, policyName := range identity.PolicyNames {
|
||||||
|
policy, err := iam.GetPolicy(policyName)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
engine := policy_engine.NewPolicyEngine()
|
||||||
|
if err := engine.SetBucketPolicy(policyName, policy.Content); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
result := engine.EvaluatePolicy(policyName, &policy_engine.PolicyEvaluationArgs{
|
||||||
|
Action: s3Action,
|
||||||
|
Resource: resource,
|
||||||
|
Principal: principal,
|
||||||
|
Conditions: conditions,
|
||||||
|
Claims: identity.Claims,
|
||||||
|
})
|
||||||
|
|
||||||
|
if result == policy_engine.PolicyResultDeny {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if result == policy_engine.PolicyResultAllow {
|
||||||
|
explicitAllow = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return explicitAllow
|
||||||
|
}
|
||||||
|
|
||||||
// VerifyActionPermission checks if the identity is allowed to perform the action on the resource.
|
// VerifyActionPermission checks if the identity is allowed to perform the action on the resource.
|
||||||
// It handles both traditional identities (via Actions) and IAM/STS identities (via Policy).
|
// It handles both traditional identities (via Actions) and IAM/STS identities (via Policy).
|
||||||
func (iam *IdentityAccessManagement) VerifyActionPermission(r *http.Request, identity *Identity, action Action, bucket, object string) s3err.ErrorCode {
|
func (iam *IdentityAccessManagement) VerifyActionPermission(r *http.Request, identity *Identity, action Action, bucket, object string) s3err.ErrorCode {
|
||||||
@@ -1679,6 +1726,7 @@ func (iam *IdentityAccessManagement) VerifyActionPermission(r *http.Request, ide
|
|||||||
return iam.authorizeWithIAM(r, identity, action, bucket, object)
|
return iam.authorizeWithIAM(r, identity, action, bucket, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Traditional actions-based authorization from static S3 config.
|
||||||
if len(identity.Actions) > 0 {
|
if len(identity.Actions) > 0 {
|
||||||
if !identity.CanDo(action, bucket, object) {
|
if !identity.CanDo(action, bucket, object) {
|
||||||
return s3err.ErrAccessDenied
|
return s3err.ErrAccessDenied
|
||||||
@@ -1686,6 +1734,14 @@ func (iam *IdentityAccessManagement) VerifyActionPermission(r *http.Request, ide
|
|||||||
return s3err.ErrNone
|
return s3err.ErrNone
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IAM policy fallback for identities with attached policies but without IAM integration.
|
||||||
|
if len(identity.PolicyNames) > 0 {
|
||||||
|
if iam.evaluateIAMPolicies(r, identity, action, bucket, object) {
|
||||||
|
return s3err.ErrNone
|
||||||
|
}
|
||||||
|
return s3err.ErrAccessDenied
|
||||||
|
}
|
||||||
|
|
||||||
return s3err.ErrAccessDenied
|
return s3err.ErrAccessDenied
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
package s3api
|
package s3api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -9,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/seaweedfs/seaweedfs/weed/credential"
|
"github.com/seaweedfs/seaweedfs/weed/credential"
|
||||||
. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
||||||
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||||
"github.com/seaweedfs/seaweedfs/weed/util/wildcard"
|
"github.com/seaweedfs/seaweedfs/weed/util/wildcard"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
@@ -260,6 +263,130 @@ func TestMatchWildcardPattern(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestVerifyActionPermissionPolicyFallback(t *testing.T) {
|
||||||
|
buildRequest := func(t *testing.T, method string) *http.Request {
|
||||||
|
t.Helper()
|
||||||
|
req, err := http.NewRequest(method, "http://s3.amazonaws.com/test-bucket/test-object", nil)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("policy allow grants access", func(t *testing.T) {
|
||||||
|
iam := &IdentityAccessManagement{}
|
||||||
|
err := iam.PutPolicy("allowGet", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::test-bucket/*"}]}`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
identity := &Identity{
|
||||||
|
Name: "policy-user",
|
||||||
|
Account: &AccountAdmin,
|
||||||
|
PolicyNames: []string{"allowGet"},
|
||||||
|
}
|
||||||
|
|
||||||
|
errCode := iam.VerifyActionPermission(buildRequest(t, http.MethodGet), identity, Action(ACTION_READ), "test-bucket", "test-object")
|
||||||
|
assert.Equal(t, s3err.ErrNone, errCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("explicit deny overrides allow", func(t *testing.T) {
|
||||||
|
iam := &IdentityAccessManagement{}
|
||||||
|
err := iam.PutPolicy("allowAllGet", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::test-bucket/*"}]}`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = iam.PutPolicy("denySecret", `{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"s3:GetObject","Resource":"arn:aws:s3:::test-bucket/secret.txt"}]}`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
identity := &Identity{
|
||||||
|
Name: "policy-user",
|
||||||
|
Account: &AccountAdmin,
|
||||||
|
PolicyNames: []string{"allowAllGet", "denySecret"},
|
||||||
|
}
|
||||||
|
|
||||||
|
errCode := iam.VerifyActionPermission(buildRequest(t, http.MethodGet), identity, Action(ACTION_READ), "test-bucket", "secret.txt")
|
||||||
|
assert.Equal(t, s3err.ErrAccessDenied, errCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("implicit deny when no statement matches", func(t *testing.T) {
|
||||||
|
iam := &IdentityAccessManagement{}
|
||||||
|
err := iam.PutPolicy("allowOtherBucket", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::other-bucket/*"}]}`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
identity := &Identity{
|
||||||
|
Name: "policy-user",
|
||||||
|
Account: &AccountAdmin,
|
||||||
|
PolicyNames: []string{"allowOtherBucket"},
|
||||||
|
}
|
||||||
|
|
||||||
|
errCode := iam.VerifyActionPermission(buildRequest(t, http.MethodGet), identity, Action(ACTION_READ), "test-bucket", "test-object")
|
||||||
|
assert.Equal(t, s3err.ErrAccessDenied, errCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid policy document does not allow", func(t *testing.T) {
|
||||||
|
iam := &IdentityAccessManagement{}
|
||||||
|
err := iam.PutPolicy("invalidPolicy", "{not-json")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
identity := &Identity{
|
||||||
|
Name: "policy-user",
|
||||||
|
Account: &AccountAdmin,
|
||||||
|
PolicyNames: []string{"invalidPolicy"},
|
||||||
|
}
|
||||||
|
|
||||||
|
errCode := iam.VerifyActionPermission(buildRequest(t, http.MethodGet), identity, Action(ACTION_READ), "test-bucket", "test-object")
|
||||||
|
assert.Equal(t, s3err.ErrAccessDenied, errCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("notresource excludes denied object", func(t *testing.T) {
|
||||||
|
iam := &IdentityAccessManagement{}
|
||||||
|
err := iam.PutPolicy("denyNotResource", `{"Version":"2012-10-17","Statement":[{"Effect":"Deny","Action":"s3:GetObject","NotResource":"arn:aws:s3:::test-bucket/public/*"}]}`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
err = iam.PutPolicy("allowAllGet", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::test-bucket/*"}]}`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
identity := &Identity{
|
||||||
|
Name: "policy-user",
|
||||||
|
Account: &AccountAdmin,
|
||||||
|
PolicyNames: []string{"allowAllGet", "denyNotResource"},
|
||||||
|
}
|
||||||
|
|
||||||
|
errCode := iam.VerifyActionPermission(buildRequest(t, http.MethodGet), identity, Action(ACTION_READ), "test-bucket", "private/secret.txt")
|
||||||
|
assert.Equal(t, s3err.ErrAccessDenied, errCode)
|
||||||
|
|
||||||
|
errCode = iam.VerifyActionPermission(buildRequest(t, http.MethodGet), identity, Action(ACTION_READ), "test-bucket", "public/readme.txt")
|
||||||
|
assert.Equal(t, s3err.ErrNone, errCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("condition securetransport enforced", func(t *testing.T) {
|
||||||
|
iam := &IdentityAccessManagement{}
|
||||||
|
err := iam.PutPolicy("allowTLSOnly", `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"arn:aws:s3:::test-bucket/*","Condition":{"Bool":{"aws:SecureTransport":"true"}}}]}`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
identity := &Identity{
|
||||||
|
Name: "policy-user",
|
||||||
|
Account: &AccountAdmin,
|
||||||
|
PolicyNames: []string{"allowTLSOnly"},
|
||||||
|
}
|
||||||
|
|
||||||
|
httpReq := buildRequest(t, http.MethodGet)
|
||||||
|
errCode := iam.VerifyActionPermission(httpReq, identity, Action(ACTION_READ), "test-bucket", "test-object")
|
||||||
|
assert.Equal(t, s3err.ErrAccessDenied, errCode)
|
||||||
|
|
||||||
|
httpsReq := buildRequest(t, http.MethodGet)
|
||||||
|
httpsReq.TLS = &tls.ConnectionState{}
|
||||||
|
errCode = iam.VerifyActionPermission(httpsReq, identity, Action(ACTION_READ), "test-bucket", "test-object")
|
||||||
|
assert.Equal(t, s3err.ErrNone, errCode)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("actions based path still works", func(t *testing.T) {
|
||||||
|
iam := &IdentityAccessManagement{}
|
||||||
|
identity := &Identity{
|
||||||
|
Name: "legacy-user",
|
||||||
|
Account: &AccountAdmin,
|
||||||
|
Actions: []Action{"Read:test-bucket"},
|
||||||
|
}
|
||||||
|
|
||||||
|
errCode := iam.VerifyActionPermission(buildRequest(t, http.MethodGet), identity, Action(ACTION_READ), "test-bucket", "any-object")
|
||||||
|
assert.Equal(t, s3err.ErrNone, errCode)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type LoadS3ApiConfigurationTestCase struct {
|
type LoadS3ApiConfigurationTestCase struct {
|
||||||
pbAccount *iam_pb.Account
|
pbAccount *iam_pb.Account
|
||||||
pbIdent *iam_pb.Identity
|
pbIdent *iam_pb.Identity
|
||||||
|
|||||||
Reference in New Issue
Block a user