mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-08-16 12:16:36 +00:00
* s3: log the requester's principal ARN in the audit entry An STS session authenticates as an opaque session subject, so requester alone gave an operator no way back to the assumed role or the session name. Record the principal ARN next to the identity name and emit it as requester_arn. * s3: record the caller identity in the STS handlers AssumeRole, GetFederationToken and GetCallerIdentity verify the caller themselves and are not wrapped by the auth middleware that records the identity, so every audit entry for minting a session had an empty requester. * s3: resolve the audit principal ARN the way policy evaluation does A JWT-authenticated identity carries no PrincipalArn — the auth layer hands the principal over in a request header — so reading the field directly left requester_arn empty for OIDC callers. buildPrincipalARN is the resolver the policy path already uses: header first, then the identity's own ARN, then a synthesized user ARN for legacy identities that have none.
131 lines
5.3 KiB
Go
131 lines
5.3 KiB
Go
package s3api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/integration"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/policy"
|
|
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
|
)
|
|
|
|
// An STS session authenticates as an opaque session subject, so the audit entry
|
|
// must also carry the principal ARN — the role name and the role session name
|
|
// are only recoverable from there.
|
|
func TestAuditRequesterArnForSTSSession(t *testing.T) {
|
|
iam := &IdentityAccessManagement{
|
|
iamIntegration: &MockIAMIntegration{
|
|
validateSessionFunc: func(ctx context.Context, token string) (*sts.SessionInfo, error) {
|
|
return &sts.SessionInfo{
|
|
AssumedRoleUser: "ClientRole/dev-session",
|
|
Principal: "arn:aws:sts::000000000000:assumed-role/ClientRole/dev-session",
|
|
Subject: "47ad4828c45b3f337bc3146081ba8f0f",
|
|
SessionName: "dev-session",
|
|
Credentials: &sts.Credentials{
|
|
AccessKeyId: "ASIA0189777d42cba8e2",
|
|
SecretAccessKey: "secret",
|
|
},
|
|
ExpiresAt: time.Now().Add(time.Hour),
|
|
Policies: []string{"ClientPolicy"},
|
|
}, nil
|
|
},
|
|
},
|
|
}
|
|
|
|
// track() installs the holder before authentication runs.
|
|
outer := s3_constants.EnsureIdentityHolder(httptest.NewRequest(http.MethodGet, "http://s3/test/", nil))
|
|
|
|
identity, _, errCode := iam.validateSTSSessionToken(outer, "session-token", "ASIA0189777d42cba8e2")
|
|
require.Equal(t, s3err.ErrNone, errCode)
|
|
|
|
iam.handleAuthResult(httptest.NewRecorder(), outer, identity, s3err.ErrNone, func(http.ResponseWriter, *http.Request) {})
|
|
|
|
log := s3err.GetAccessLog(outer, http.StatusOK, s3err.ErrNone)
|
|
assert.Equal(t, "47ad4828c45b3f337bc3146081ba8f0f", log.Requester)
|
|
assert.Equal(t, "arn:aws:sts::000000000000:assumed-role/ClientRole/dev-session", log.RequesterArn,
|
|
"audit entry must name the assumed role and session")
|
|
}
|
|
|
|
// A JWT-authenticated identity carries no PrincipalArn of its own — the auth
|
|
// layer hands the principal over in a request header — so the audit entry has to
|
|
// resolve the ARN the same way policy evaluation does.
|
|
func TestAuditRequesterArnForJWTIdentity(t *testing.T) {
|
|
iam := &IdentityAccessManagement{}
|
|
|
|
outer := s3_constants.EnsureIdentityHolder(httptest.NewRequest(http.MethodGet, "http://s3/test/", nil))
|
|
outer.Header.Set(s3_constants.SeaweedFSPrincipalHeader, "arn:aws:sts::000000000000:assumed-role/ClientRole/oidc-session")
|
|
|
|
identity := &Identity{Name: "alice", Account: &Account{Id: "alice"}}
|
|
iam.handleAuthResult(httptest.NewRecorder(), outer, identity, s3err.ErrNone, func(http.ResponseWriter, *http.Request) {})
|
|
|
|
log := s3err.GetAccessLog(outer, http.StatusOK, s3err.ErrNone)
|
|
assert.Equal(t, "alice", log.Requester)
|
|
assert.Equal(t, "arn:aws:sts::000000000000:assumed-role/ClientRole/oidc-session", log.RequesterArn)
|
|
}
|
|
|
|
// The AssumeRole call itself is authenticated inside the STS handler, which the
|
|
// generic auth middleware never wraps; without recording the caller there the
|
|
// audit entry for minting a session has no requester at all.
|
|
func TestAuditRequesterForAssumeRole(t *testing.T) {
|
|
ctx := context.Background()
|
|
manager := newTestSTSIntegrationManager(t)
|
|
|
|
require.NoError(t, manager.CreatePolicy(ctx, "", "ClientPolicy", &policy.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy.Statement{{
|
|
Effect: "Allow",
|
|
Action: []string{"s3:*"},
|
|
Resource: []string{"arn:aws:s3:::*", "arn:aws:s3:::*/*"},
|
|
}},
|
|
}))
|
|
require.NoError(t, manager.CreateRole(ctx, "", "ClientRole", &integration.RoleDefinition{
|
|
RoleName: "ClientRole",
|
|
TrustPolicy: &policy.PolicyDocument{
|
|
Version: "2012-10-17",
|
|
Statement: []policy.Statement{{Effect: "Allow", Principal: "*", Action: []string{"sts:AssumeRole"}}},
|
|
},
|
|
AttachedPolicies: []string{"ClientPolicy"},
|
|
}))
|
|
|
|
const accessKey, secretKey = "adminkey", "adminsecret"
|
|
iam := &IdentityAccessManagement{iamIntegration: NewS3IAMIntegration(manager, "")}
|
|
require.NoError(t, iam.loadS3ApiConfiguration(&iam_pb.S3ApiConfiguration{
|
|
Identities: []*iam_pb.Identity{{
|
|
Name: "admin",
|
|
Credentials: []*iam_pb.Credential{{AccessKey: accessKey, SecretKey: secretKey}},
|
|
Actions: []string{"Admin"},
|
|
}},
|
|
}))
|
|
|
|
body := url.Values{
|
|
"Action": {"AssumeRole"},
|
|
"Version": {"2011-06-15"},
|
|
"RoleArn": {"arn:aws:iam::" + defaultAccountID + ":role/ClientRole"},
|
|
"RoleSessionName": {"dev-session"},
|
|
}.Encode()
|
|
req, err := newTestRequest(http.MethodPost, "http://sts.seaweedfs.test/", int64(len(body)), strings.NewReader(body))
|
|
require.NoError(t, err)
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
require.NoError(t, signRequestV4(req, accessKey, secretKey))
|
|
req = s3_constants.EnsureIdentityHolder(req)
|
|
|
|
rec := httptest.NewRecorder()
|
|
NewSTSHandlers(manager.GetSTSService(), iam).handleAssumeRole(rec, req)
|
|
require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
|
|
|
|
log := s3err.GetAccessLog(req, rec.Code, s3err.ErrNone)
|
|
assert.Equal(t, "admin", log.Requester, "AssumeRole must audit who asked for the session")
|
|
assert.Equal(t, "arn:aws:iam::"+defaultAccountID+":user/admin", log.RequesterArn)
|
|
}
|