Files
seaweedfs/weed/iamapi/iamapi_authz_test.go
Chris LuandGitHub e8020910db iam: authorize IAM management actions as IAM actions (#10593)
* s3: keep a non-S3 action out of the request-shape resolver

ResolveS3Action reads the request shape before it looks at the base action, so
an iam: or sts: action on a request that happens to carry an S3 query parameter
came back as the S3 action for that parameter. An action that already names its
service is resolved; there is no S3 request shape to read for it.

* iam: authorize the standalone IAM server's actions as IAM, not as S3

The standalone `weed iam` server wrapped its single POST / route in the generic
S3 Auth middleware with ACTION_ADMIN. The route has no {bucket}, so the check
ran with an empty bucket and resolved to a coarse S3 action rather than the IAM
one. The embedded IAM surface checks iam:<Action>; the standalone one was never
updated to match.

Both now go through one authorization function, so they cannot drift apart
again. It also rejects the anonymous identity, which has no user of its own to
run a self-service action against, and reads UserName from the body only, where
the handlers read it from.
2026-08-05 17:08:21 -07:00

144 lines
4.9 KiB
Go

package iamapi
import (
"context"
"crypto/sha256"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
"github.com/gorilla/mux"
"github.com/seaweedfs/seaweedfs/weed/credential"
"github.com/seaweedfs/seaweedfs/weed/credential/memory"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// A data-plane identity holding nothing but the equivalent of AmazonS3FullAccess,
// and an admin, so the same request can be run from both sides. The admin key is
// obviously fake to keep secret scanners quiet.
const authzConfigJSON = `{
"identities": [
{
"name": "power_user",
"credentials": [{"accessKey": "AKIAIOSFODNN7EXAMPLE", "secretKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"}],
"policyNames": ["PowerUserPolicy"]
},
{
"name": "iam_admin",
"credentials": [{"accessKey": "AKIATESTFAKEADMIN0001", "secretKey": "testAdminSecretFake0000000000000000000000"}],
"actions": ["Admin"]
}
],
"policies": [
{
"name": "PowerUserPolicy",
"content": "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Action\":\"s3:*\",\"Resource\":\"*\"}]}"
}
]
}`
func newAuthzTestServer(t *testing.T) *httptest.Server {
t.Helper()
config := &iam_pb.S3ApiConfiguration{}
require.NoError(t, filer.ParseS3ConfigurationFromBytes([]byte(authzConfigJSON), config))
store := &memory.MemoryStore{}
require.NoError(t, store.Initialize(nil, ""))
cm := &credential.CredentialManager{Store: store}
require.NoError(t, cm.SaveConfiguration(context.Background(), config))
iam := &s3api.IdentityAccessManagement{}
iam.SetCredentialManagerForTest(cm)
require.NoError(t, iam.LoadS3ApiConfigurationFromBytes([]byte(authzConfigJSON)))
iama := &IamApiServer{
iam: iam,
s3ApiConfig: &countingConfigSaver{cm: cm},
}
router := mux.NewRouter().SkipClean(true)
iama.registerRouter(router)
server := httptest.NewServer(router)
t.Cleanup(server.Close)
return server
}
func postSignedIamAction(t *testing.T, serverURL, accessKey, secretKey, rawQuery, body string) *http.Response {
t.Helper()
url := serverURL + "/"
if rawQuery != "" {
url += "?" + rawQuery
}
req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(body))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
payloadHash := fmt.Sprintf("%x", sha256.Sum256([]byte(body)))
require.NoError(t, v4.NewSigner().SignHTTP(context.Background(),
aws.Credentials{AccessKeyID: accessKey, SecretAccessKey: secretKey},
req, payloadHash, "iam", "us-east-1", time.Now()))
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
t.Cleanup(func() { resp.Body.Close() })
return resp
}
// An S3 data-plane policy grants no IAM management, however broad it is: the
// standalone server used to check these actions as a coarse S3 action, which
// such a policy matches.
func TestIamManagementDeniedForDataPlanePolicy(t *testing.T) {
server := newAuthzTestServer(t)
for _, action := range []string{"CreateUser", "CreateAccessKey", "PutUserPolicy", "AttachUserPolicy"} {
t.Run(action, func(t *testing.T) {
resp := postSignedIamAction(t, server.URL, "AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"", "Action="+action+"&UserName=victim&Version=2010-05-08")
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
})
}
}
// Same expectation when the request carries an S3 query parameter, which the
// action resolver used to read even for an IAM action.
func TestIamManagementDeniedWithS3QueryParameter(t *testing.T) {
server := newAuthzTestServer(t)
for _, rawQuery := range []string{"delete", "acl", "tagging", "policy"} {
t.Run(rawQuery, func(t *testing.T) {
resp := postSignedIamAction(t, server.URL, "AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
rawQuery, "Action=CreateUser&UserName=victim&Version=2010-05-08")
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
})
}
}
// Self-service still works without an IAM grant, matching AWS and the embedded
// IAM surface: a user may rotate its own access keys.
func TestIamSelfServiceAllowedForDataPlanePolicy(t *testing.T) {
server := newAuthzTestServer(t)
resp := postSignedIamAction(t, server.URL, "AKIAIOSFODNN7EXAMPLE", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"", "Action=ListAccessKeys&Version=2010-05-08")
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
func TestIamManagementAllowedForAdmin(t *testing.T) {
server := newAuthzTestServer(t)
resp := postSignedIamAction(t, server.URL, "AKIATESTFAKEADMIN0001", "testAdminSecretFake0000000000000000000000",
"", "Action=CreateUser&UserName=new-user&Version=2010-05-08")
assert.Equal(t, http.StatusOK, resp.StatusCode)
}