Files
seaweedfs/weed/s3api/s3api_embedded_iam_authz_test.go
Chris Lu c0a7dbb2bb iam: bind CreateServiceAccount ParentUser to the caller (#11218)
* iam: bind CreateServiceAccount target to caller in AuthorizeIamAction

A non-admin holding iam:CreateServiceAccount could pass an arbitrary
ParentUser and mint a service account for any identity, inheriting that
identity permissions. Add a self-target category so a granted non-admin
may only target their own identity; admins remain unrestricted.

* iam: authorize CreateServiceAccount against its ParentUser target

AuthIamManagement passed UserName as the authorization target for every
action, so CreateServiceAccount was authorized with an empty target and
the self-target binding never saw the caller-supplied ParentUser. Pass
ParentUser for that action so the binding takes effect on the live path.

* iam: test CreateServiceAccount binds target to caller

Regression test: a non-admin holding iam:CreateServiceAccount may target
itself but is denied targeting another identity; admins remain
unrestricted.

* iam: authorize CreateServiceAccount against ParentUser on the S3 port

UnifiedPostHandler passed UserName as the authorization target for every
IAM action, so CreateServiceAccount was authorized with an empty target
on the S3-port route and the self-target binding never saw the caller
ParentUser. Extract iamTargetUserName (ParentUser for CreateServiceAccount,
UserName otherwise) and use it from both IAM dispatch surfaces so the
binding applies on the live S3-port path as well as the standalone iam
server.

* iam: test CreateServiceAccount ParentUser binding on the S3 port

End-to-end regression test through UnifiedPostHandler: a non-admin
holding iam:CreateServiceAccount is denied (403) when targeting another
identity and passes authorization when targeting itself.
2026-09-07 20:25:03 -07:00

117 lines
5.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":["*"]}]}`
)
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, not
// UserName. A non-admin holding only iam:CreateServiceAccount must not be able
// to mint a service account for another identity (privilege escalation to that
// identity's permissions).
func TestAuthorizeIamActionCreateServiceAccountBindsTargetToCaller(t *testing.T) {
iam := newIamAuthzTestIam(t)
require.NoError(t, iam.PutPolicy("CreateSvcAcctPolicy", iamCreateSvcAcctPolicy))
dev := &Identity{Name: "dev", PolicyNames: []string{"CreateSvcAcctPolicy"}}
admin := &Identity{Name: "admin", Actions: []Action{s3_constants.ACTION_ADMIN}}
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.ErrAccessDenied,
iam.AuthorizeIamAction(iamPostRequest(""), dev, "CreateServiceAccount", "admin"))
assert.Equal(t, s3err.ErrNone,
iam.AuthorizeIamAction(iamPostRequest(""), admin, "CreateServiceAccount", "victim"))
}