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) }