mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-19 20:44:28 +02:00
* iam: scope CreateServiceAccount's ParentUser by policy resource AuthorizeIamAction evaluated the iam:CreateServiceAccount grant against a hardcoded arn:aws:iam:::* resource, so the policy's Resource never had a say; #11218 then bound the target to the caller outright. Evaluate self-target actions against the target identity's ARN instead: a wildcard resource keeps the deliberate any-target grant, a scoped resource confines issuance to the named parent identity, anything else is denied. buildResourceARN gains the ARN pass-through buildS3ResourceArn already has, so iam: ARNs reach the policy engine intact on the attached-policies route. Closes #11735 * ci: retrigger after unrelated weed/storage metrics flake * iam: fold IAM-ARN matching into awsIAMMatch, drop debug test - remove zz_debug_test.go (local debugging leftover) - fold awsIAMMatchResource into awsIAMMatch: the arn:aws:iam:: value prefix selects case-sensitive matching, so no separate name is needed - trim comments --------- Co-authored-by: batonogov <batonogov@users.noreply.github.com> Co-authored-by: Chris Lu <chris.lu@gmail.com>
148 lines
7.0 KiB
Go
148 lines
7.0 KiB
Go
package s3api
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
dataPlanePolicy = `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:*","Resource":"*"}]}`
|
|
iamAdminPolicy = `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"iam:*","Resource":"*"}]}`
|
|
iamCreateSvcAcctPolicy = `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["iam:CreateServiceAccount"],"Resource":["*"]}]}`
|
|
// The wildcard ARN shape this authorization always used; it must keep
|
|
// matching any parent so existing grants survive.
|
|
iamCreateSvcAcctWildcardArnPolicy = `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["iam:CreateServiceAccount"],"Resource":["arn:aws:iam:::*"]}]}`
|
|
// Scoped to one parent identity.
|
|
iamCreateSvcAcctZonePolicy = `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["iam:CreateServiceAccount"],"Resource":["arn:aws:iam:::user/team-parent"]}]}`
|
|
)
|
|
|
|
func newIamAuthzTestIam(t *testing.T) *IdentityAccessManagement {
|
|
t.Helper()
|
|
iam := &IdentityAccessManagement{}
|
|
require.NoError(t, iam.PutPolicy("PowerUserPolicy", dataPlanePolicy))
|
|
require.NoError(t, iam.PutPolicy("IamAdminPolicy", iamAdminPolicy))
|
|
return iam
|
|
}
|
|
|
|
func iamPostRequest(rawQuery string) *http.Request {
|
|
url := "http://s3.example.com/"
|
|
if rawQuery != "" {
|
|
url += "?" + rawQuery
|
|
}
|
|
return httptest.NewRequest(http.MethodPost, url, nil)
|
|
}
|
|
|
|
// A policy granting the S3 data plane must not reach IAM management, including
|
|
// when the request carries an S3 query parameter — the action resolver used to
|
|
// read the request shape even for an iam: action.
|
|
func TestAuthorizeIamActionDeniesDataPlanePolicy(t *testing.T) {
|
|
iam := newIamAuthzTestIam(t)
|
|
identity := &Identity{Name: "power_user", PolicyNames: []string{"PowerUserPolicy"}}
|
|
|
|
for _, rawQuery := range []string{"", "delete", "acl", "tagging", "policy", "versions"} {
|
|
t.Run("query="+rawQuery, func(t *testing.T) {
|
|
assert.Equal(t, s3err.ErrAccessDenied,
|
|
iam.AuthorizeIamAction(iamPostRequest(rawQuery), identity, "CreateUser", "victim"))
|
|
assert.Equal(t, s3err.ErrAccessDenied,
|
|
iam.AuthorizeIamAction(iamPostRequest(rawQuery), identity, "CreateAccessKey", "victim"))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthorizeIamActionAllowsIamPolicy(t *testing.T) {
|
|
iam := newIamAuthzTestIam(t)
|
|
identity := &Identity{Name: "iam_operator", PolicyNames: []string{"IamAdminPolicy"}}
|
|
|
|
for _, rawQuery := range []string{"", "delete"} {
|
|
t.Run("query="+rawQuery, func(t *testing.T) {
|
|
assert.Equal(t, s3err.ErrNone,
|
|
iam.AuthorizeIamAction(iamPostRequest(rawQuery), identity, "CreateUser", "victim"))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuthorizeIamActionSelfServiceAndAdmin(t *testing.T) {
|
|
iam := newIamAuthzTestIam(t)
|
|
powerUser := &Identity{Name: "power_user", PolicyNames: []string{"PowerUserPolicy"}}
|
|
admin := &Identity{Name: "admin", Actions: []Action{s3_constants.ACTION_ADMIN}}
|
|
|
|
assert.Equal(t, s3err.ErrNone, iam.AuthorizeIamAction(iamPostRequest(""), powerUser, "CreateAccessKey", ""))
|
|
assert.Equal(t, s3err.ErrNone, iam.AuthorizeIamAction(iamPostRequest(""), powerUser, "CreateAccessKey", "power_user"))
|
|
assert.Equal(t, s3err.ErrNone, iam.AuthorizeIamAction(iamPostRequest(""), admin, "CreateUser", "victim"))
|
|
assert.Equal(t, s3err.ErrAccessDenied, iam.AuthorizeIamAction(iamPostRequest(""), nil, "CreateUser", "victim"))
|
|
}
|
|
|
|
// An identity with no grant at all is the negative control: the route itself
|
|
// was never open, so a denial here has to come from the policy check.
|
|
func TestAuthorizeIamActionDeniesUngrantedIdentity(t *testing.T) {
|
|
iam := newIamAuthzTestIam(t)
|
|
identity := &Identity{Name: "nobody"}
|
|
|
|
assert.Equal(t, s3err.ErrAccessDenied,
|
|
iam.AuthorizeIamAction(iamPostRequest(""), identity, "CreateUser", "victim"))
|
|
}
|
|
|
|
// A configured anonymous identity must not slip in through the self-service
|
|
// carve-out, which would otherwise hand it the caller-implied user name.
|
|
func TestAuthorizeIamActionDeniesAnonymous(t *testing.T) {
|
|
iam := newIamAuthzTestIam(t)
|
|
anonymous := &Identity{Name: s3_constants.AccountAnonymousId, PolicyNames: []string{"IamAdminPolicy"}}
|
|
|
|
assert.Equal(t, s3err.ErrAccessDenied,
|
|
iam.AuthorizeIamAction(iamPostRequest(""), anonymous, "CreateAccessKey", ""))
|
|
assert.Equal(t, s3err.ErrAccessDenied,
|
|
iam.AuthorizeIamAction(iamPostRequest(""), anonymous, "CreateUser", "victim"))
|
|
}
|
|
|
|
// CreateServiceAccount takes its target from the ParentUser parameter. A
|
|
// non-admin holding iam:CreateServiceAccount targets the identities its
|
|
// policy resource covers: a wildcard resource allows any target, a scoped
|
|
// resource confines the grant to the named parent identity.
|
|
func TestAuthorizeIamActionCreateServiceAccountScopedByPolicyResource(t *testing.T) {
|
|
iam := newIamAuthzTestIam(t)
|
|
require.NoError(t, iam.PutPolicy("CreateSvcAcctPolicy", iamCreateSvcAcctPolicy))
|
|
require.NoError(t, iam.PutPolicy("CreateSvcAcctWildcardArnPolicy", iamCreateSvcAcctWildcardArnPolicy))
|
|
require.NoError(t, iam.PutPolicy("CreateSvcAcctZonePolicy", iamCreateSvcAcctZonePolicy))
|
|
dev := &Identity{Name: "dev", PolicyNames: []string{"CreateSvcAcctPolicy"}}
|
|
wildcardArn := &Identity{Name: "wildcard-arn", PolicyNames: []string{"CreateSvcAcctWildcardArnPolicy"}}
|
|
keymaker := &Identity{Name: "keymaker", PolicyNames: []string{"CreateSvcAcctZonePolicy"}}
|
|
admin := &Identity{Name: "admin", Actions: []Action{s3_constants.ACTION_ADMIN}}
|
|
|
|
// Wildcard resource: any target, including the caller itself.
|
|
assert.Equal(t, s3err.ErrNone,
|
|
iam.AuthorizeIamAction(iamPostRequest(""), dev, "CreateServiceAccount", "dev"))
|
|
assert.Equal(t, s3err.ErrNone,
|
|
iam.AuthorizeIamAction(iamPostRequest(""), dev, "CreateServiceAccount", ""))
|
|
assert.Equal(t, s3err.ErrNone,
|
|
iam.AuthorizeIamAction(iamPostRequest(""), dev, "CreateServiceAccount", "admin"))
|
|
|
|
// Wildcard ARN: any target.
|
|
assert.Equal(t, s3err.ErrNone,
|
|
iam.AuthorizeIamAction(iamPostRequest(""), wildcardArn, "CreateServiceAccount", "team-parent"))
|
|
assert.Equal(t, s3err.ErrNone,
|
|
iam.AuthorizeIamAction(iamPostRequest(""), wildcardArn, "CreateServiceAccount", "admin"))
|
|
|
|
// Scoped resource: the named parent only. IAM resource matching is
|
|
// case-sensitive, so a case variant is a different identity.
|
|
assert.Equal(t, s3err.ErrNone,
|
|
iam.AuthorizeIamAction(iamPostRequest(""), keymaker, "CreateServiceAccount", "team-parent"))
|
|
assert.Equal(t, s3err.ErrAccessDenied,
|
|
iam.AuthorizeIamAction(iamPostRequest(""), keymaker, "CreateServiceAccount", "Team-Parent"))
|
|
assert.Equal(t, s3err.ErrAccessDenied,
|
|
iam.AuthorizeIamAction(iamPostRequest(""), keymaker, "CreateServiceAccount", "admin"))
|
|
assert.Equal(t, s3err.ErrAccessDenied,
|
|
iam.AuthorizeIamAction(iamPostRequest(""), keymaker, "CreateServiceAccount", "keymaker"))
|
|
assert.Equal(t, s3err.ErrAccessDenied,
|
|
iam.AuthorizeIamAction(iamPostRequest(""), keymaker, "CreateServiceAccount", ""))
|
|
|
|
// Admins remain unrestricted.
|
|
assert.Equal(t, s3err.ErrNone,
|
|
iam.AuthorizeIamAction(iamPostRequest(""), admin, "CreateServiceAccount", "victim"))
|
|
}
|