diff --git a/test/s3tables/unity_catalog/setup_test.go b/test/s3tables/unity_catalog/setup_test.go index f3351c441..b56044cb4 100644 --- a/test/s3tables/unity_catalog/setup_test.go +++ b/test/s3tables/unity_catalog/setup_test.go @@ -26,6 +26,10 @@ const ( ucAPIBase = "/api/2.1/unity-catalog" ucWarehouse = "lakehouse" ucWarehouseKey = "warehouse" + // A non-empty s3.sessionToken.N puts UC's credential vendor on its + // StaticAwsCredentialGenerator path: it vends the configured access key, + // secret key and this token verbatim instead of calling AWS STS. + ucVendedSessionToken = "unity-catalog-vended-session-token" // Role used by the master-role STS-vended variant of the test. The trust // policy is wide open so any caller can assume it; in production UC @@ -65,6 +69,9 @@ type ucServerOpts struct { // MasterRoleArn populates aws.masterRoleArn. Empty means UC falls back to // static aws.accessKey / aws.secretKey for storage operations. MasterRoleArn string + // SessionToken populates s3.sessionToken.0, selecting UC's static + // credential generator for the per-bucket config. + SessionToken string // ExtraEnv adds environment variables to the UC container, useful for // AWS_ENDPOINT_URL_STS-style overrides. ExtraEnv map[string]string @@ -220,6 +227,7 @@ func (env *testEnv) startUnityCatalog(t *testing.T, ctx context.Context, opts uc fmt.Sprintf("s3.awsRoleArn.0=%s", opts.MasterRoleArn), fmt.Sprintf("s3.accessKey.0=%s", env.accessKey), fmt.Sprintf("s3.secretKey.0=%s", env.secretKey), + fmt.Sprintf("s3.sessionToken.0=%s", opts.SessionToken), fmt.Sprintf("s3.endpoint.0=%s", s3EndpointForContainer), "", }, "\n") diff --git a/test/s3tables/unity_catalog/unity_catalog_test.go b/test/s3tables/unity_catalog/unity_catalog_test.go index 35b48be09..97b024815 100644 --- a/test/s3tables/unity_catalog/unity_catalog_test.go +++ b/test/s3tables/unity_catalog/unity_catalog_test.go @@ -8,6 +8,7 @@ package unity_catalog import ( "context" "fmt" + "strings" "testing" "time" @@ -46,7 +47,7 @@ func TestUnityCatalogDeltaIntegration(t *testing.T) { } t.Log(">>> starting Unity Catalog server (static keys)...") - env.startUnityCatalog(t, ctx, ucServerOpts{}) + env.startUnityCatalog(t, ctx, ucServerOpts{SessionToken: ucVendedSessionToken}) t.Log(">>> Unity Catalog ready") uc := newUCClient(fmt.Sprintf("http://127.0.0.1:%d", env.ucHostPort)) @@ -145,28 +146,41 @@ func TestUnityCatalogDeltaIntegration(t *testing.T) { } }) - t.Run("TemporaryTableCredentialsRejected", func(t *testing.T) { - // With aws.masterRoleArn empty AND no s3.sessionToken.0 set, UC OSS - // always tries to AssumeRole via its internal StsClient (see - // AwsCredentialVendor.createPerBucketCredentialGenerator). Against a - // non-AWS endpoint, that call doesn't reach a real STS, so UC returns - // "S3 bucket configuration not found." or an STS-side error. This is - // the gap users hit at : - // "with simple S3 access and secret keys, Unity Catalog does not seem - // to work." - // - // The assertion is therefore inverted: we expect a non-nil error from - // /temporary-table-credentials with this configuration. A future - // variant can pin s3.sessionToken.0 (UC's StaticAwsCredentialGenerator - // path) once SeaweedFS' SigV4 path tolerates the vended session token. + t.Run("TemporaryTableCredentials", func(t *testing.T) { + // s3.sessionToken.0 is set, so UC's AwsCredentialVendor takes the + // StaticAwsCredentialGenerator path and vends the configured keys + // verbatim, token included — no STS call at all. SeaweedFS authenticates + // the request on the static access key and tolerates the foreign token. if createdTable.TableID == "" { t.Fatalf("created table has empty table_id; cannot request temporary credentials") } - _, err := uc.generateTemporaryTableCredentials(ctx, createdTable.TableID, "READ_WRITE") - if err == nil { - t.Fatalf("expected /temporary-table-credentials to fail with the static-key playground configuration; it succeeded unexpectedly") + creds, err := uc.generateTemporaryTableCredentials(ctx, createdTable.TableID, "READ_WRITE") + if err != nil { + t.Fatalf("temporary-table-credentials: %v", err) + } + awsCreds := creds.AwsTempCredentials + if awsCreds == nil || awsCreds.AccessKeyID == "" || awsCreds.SessionToken == "" { + t.Fatalf("expected aws_temp_credentials with a session_token, got %+v", creds) + } + if awsCreds.AccessKeyID != env.accessKey || awsCreds.SessionToken != ucVendedSessionToken { + t.Fatalf("expected UC to vend the configured static credential, got %+v", awsCreds) + } + + s3v := env.newHostS3ClientWithCreds(t, ctx, awsCreds.AccessKeyID, awsCreds.SecretAccessKey, awsCreds.SessionToken) + probeKey := fmt.Sprintf("%s/%s/%s/vended.txt", ucWarehouseKey, schemaName, tableName) + if _, err := s3v.PutObject(ctx, &s3.PutObjectInput{ + Bucket: aws.String(ucWarehouse), + Key: aws.String(probeKey), + Body: strings.NewReader("ok"), + }); err != nil { + t.Fatalf("PutObject with UC-vended credentials: %v", err) + } + if _, err := s3v.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: aws.String(ucWarehouse), + Key: aws.String(probeKey), + }); err != nil { + t.Fatalf("DeleteObject with UC-vended credentials: %v", err) } - t.Logf("expected failure (UC static-key path requires AWS STS): %v", err) }) t.Run("DeleteTableSchemaCatalog", func(t *testing.T) { diff --git a/weed/s3api/auth_credentials.go b/weed/s3api/auth_credentials.go index bb34f0bd4..6c47005ba 100644 --- a/weed/s3api/auth_credentials.go +++ b/weed/s3api/auth_credentials.go @@ -2674,13 +2674,7 @@ func (iam *IdentityAccessManagement) isActionExplicitlyDeniedByIAM(r *http.Reque // A chained caller authenticates with an STS session token whose inline // session policy can also carry an explicit deny. - sessionToken := r.Header.Get(s3_constants.SeaweedFSSessionTokenHeader) - if sessionToken == "" { - sessionToken = r.Header.Get("X-Amz-Security-Token") - if sessionToken == "" { - sessionToken = r.URL.Query().Get("X-Amz-Security-Token") - } - } + sessionToken := extractSessionToken(r) if len(policyNames) == 0 && sessionToken == "" { return false @@ -2748,12 +2742,26 @@ func (iam *IdentityAccessManagement) attachedPolicyNames(identity *Identity) []s return names } +// extractSessionToken returns the request's session token, whichever transport +// carried it: the internal header set after JWT authentication, the +// X-Amz-Security-Token header, or the presigned-URL query parameter. +func extractSessionToken(r *http.Request) string { + if s3_constants.IsSessionTokenIgnored(r.Context()) { + return "" + } + if token := r.Header.Get(s3_constants.SeaweedFSSessionTokenHeader); token != "" { + return token + } + if token := r.Header.Get("X-Amz-Security-Token"); token != "" { + return token + } + return r.URL.Query().Get("X-Amz-Security-Token") +} + // hasSessionToken reports whether the request carries an STS session token, // whose session policies are known only to the IAM integration. func hasSessionToken(r *http.Request) bool { - return r.Header.Get(s3_constants.SeaweedFSSessionTokenHeader) != "" || - r.Header.Get("X-Amz-Security-Token") != "" || - r.URL.Query().Get("X-Amz-Security-Token") != "" + return extractSessionToken(r) != "" } // authorizationRoute picks the mechanism, so every caller routes identically. @@ -3021,22 +3029,11 @@ func (iam *IdentityAccessManagement) authorizeWithIAM(r *http.Request, identity iam.primeBucketForIAM(bucket) } - // Get session info from request headers - // First check for JWT-based authentication headers (SeaweedFSSessionTokenHeader) - sessionToken := r.Header.Get(s3_constants.SeaweedFSSessionTokenHeader) + // JWT authentication records its token in SeaweedFSSessionTokenHeader; + // SigV4 requests carry it as X-Amz-Security-Token. + sessionToken := extractSessionToken(r) principal := r.Header.Get(s3_constants.SeaweedFSPrincipalHeader) - // Fallback to AWS Signature V4 STS token if JWT token not present - // This handles the case where STS AssumeRoleWithWebIdentity generates temporary credentials - // that include an X-Amz-Security-Token header (in addition to the access key and secret) - if sessionToken == "" { - sessionToken = r.Header.Get("X-Amz-Security-Token") - if sessionToken == "" { - // Also check query parameters for presigned URLs with STS tokens - sessionToken = r.URL.Query().Get("X-Amz-Security-Token") - } - } - policyNames := iam.attachedPolicyNames(identity) iamIdentity := &IAMIdentity{ diff --git a/weed/s3api/auth_signature_v4.go b/weed/s3api/auth_signature_v4.go index 7dc76ada1..8caae3e97 100644 --- a/weed/s3api/auth_signature_v4.go +++ b/weed/s3api/auth_signature_v4.go @@ -265,38 +265,40 @@ func (iam *IdentityAccessManagement) verifyV4Signature(r *http.Request, shouldCh var cred *Credential - // 2. Check for STS session token - sessionToken := r.Header.Get("X-Amz-Security-Token") - if sessionToken == "" { - sessionToken = r.URL.Query().Get("X-Amz-Security-Token") - } - if sessionToken != "" { - // Validate STS session token - identity, cred, errCode = iam.validateSTSSessionToken(r, sessionToken, authInfo.AccessKey) - if errCode != s3err.ErrNone { - return nil, nil, "", nil, errCode - } - } else { - // 3. Lookup user and credentials - var found bool - identity, cred, found = iam.lookupByAccessKey(authInfo.AccessKey) - if !found { - // Log detailed error information for InvalidAccessKeyId (avoid slice allocation for performance) - iam.m.RLock() - keyCount := len(iam.accessKeyIdent) - iam.m.RUnlock() - - glog.Warningf("InvalidAccessKeyId: attempted key '%s' not found. Available keys: %d, Auth enabled: %v", - authInfo.AccessKey, keyCount, iam.isAuthEnabled) - return nil, nil, "", nil, s3err.ErrInvalidAccessKeyID - } - + // 2. Resolve the credential. A configured access key wins over a session + // token: credential vendors like Unity Catalog emit a token with static + // credentials too, and it adds nothing the signature does not prove. + // STS-issued access keys are never registered in the static map, so they + // still land on session-token validation. + sessionToken := extractSessionToken(r) + var found bool + identity, cred, found = iam.lookupByAccessKey(authInfo.AccessKey) + switch { + case found: // Check service account expiration if cred.isCredentialExpired() { glog.V(2).Infof("Service account credential %s has expired (expiration: %d, now: %d)", authInfo.AccessKey, cred.Expiration, time.Now().Unix()) return nil, nil, "", nil, s3err.ErrAccessDenied } + if sessionToken != "" { + *r = *r.WithContext(s3_constants.IgnoreSessionTokenInContext(r.Context())) + } + case sessionToken != "": + // Validate STS session token + identity, cred, errCode = iam.validateSTSSessionToken(r, sessionToken, authInfo.AccessKey) + if errCode != s3err.ErrNone { + return nil, nil, "", nil, errCode + } + default: + // Log detailed error information for InvalidAccessKeyId (avoid slice allocation for performance) + iam.m.RLock() + keyCount := len(iam.accessKeyIdent) + iam.m.RUnlock() + + glog.Warningf("InvalidAccessKeyId: attempted key '%s' not found. Available keys: %d, Auth enabled: %v", + authInfo.AccessKey, keyCount, iam.isAuthEnabled) + return nil, nil, "", nil, s3err.ErrInvalidAccessKeyID } // 3. Perform permission check diff --git a/weed/s3api/auth_static_session_token_test.go b/weed/s3api/auth_static_session_token_test.go new file mode 100644 index 000000000..44d1f08af --- /dev/null +++ b/weed/s3api/auth_static_session_token_test.go @@ -0,0 +1,121 @@ +package s3api + +import ( + "net/http" + "sync" + "testing" + "time" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" + "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" +) + +// Credential vendors like Unity Catalog emit a session token even for static +// credentials. A request signed by a configured access key must authenticate +// as that identity, tolerating the foreign token. +func TestStaticCredentialWithSessionToken(t *testing.T) { + const token = "vended-static-session-token" + + newSigned := func(t *testing.T) *http.Request { + req := mustNewRequest(http.MethodGet, "http://127.0.0.1:9000/bucket/object", 0, nil, t) + req.Header.Set("X-Amz-Security-Token", token) + require.NoError(t, signRequestV4(req, "access_key_1", "secret_key_1")) + return mux.SetURLVars(req, map[string]string{"bucket": "bucket", "object": "object"}) + } + + t.Run("authenticates as the static identity", func(t *testing.T) { + iam := newTestIAMWithCreds() + r := newSigned(t) + + identity, errCode := iam.reqSignatureV4Verify(r) + require.Equal(t, s3err.ErrNone, errCode) + assert.Equal(t, "someone", identity.Name) + assert.True(t, s3_constants.IsSessionTokenIgnored(r.Context())) + assert.False(t, hasSessionToken(r), "tolerated token must be inert for authorization") + }) + + t.Run("authorized through the static action path", func(t *testing.T) { + iam := newTestIAMWithCreds() + r := newSigned(t) + + _, errCode := iam.authRequest(r, s3_constants.ACTION_READ) + assert.Equal(t, s3err.ErrNone, errCode) + }) + + t.Run("repeat verification stays idempotent", func(t *testing.T) { + iam := newTestIAMWithCreds() + r := newSigned(t) + + _, errCode := iam.reqSignatureV4Verify(r) + require.Equal(t, s3err.ErrNone, errCode) + // Handlers re-authenticate the same request (e.g. PutObjectAcl). + _, errCode = iam.reqSignatureV4Verify(r) + assert.Equal(t, s3err.ErrNone, errCode) + }) + + t.Run("unsigned token is still rejected", func(t *testing.T) { + iam := newTestIAMWithCreds() + r := mustNewSignedRequest(http.MethodGet, "http://127.0.0.1:9000/bucket/object", 0, nil, t) + r.Header.Set("X-Amz-Security-Token", token) + + _, errCode := iam.reqSignatureV4Verify(r) + assert.Equal(t, s3err.ErrSignatureDoesNotMatch, errCode) + }) + + t.Run("unknown access key with token still rejected", func(t *testing.T) { + iam := newTestIAMWithCreds() + req := mustNewRequest(http.MethodGet, "http://127.0.0.1:9000/bucket/object", 0, nil, t) + req.Header.Set("X-Amz-Security-Token", token) + require.NoError(t, signRequestV4(req, "no_such_key", "secret_key_1")) + + _, errCode := iam.reqSignatureV4Verify(req) + assert.Equal(t, s3err.ErrInvalidAccessKeyID, errCode) + }) + + t.Run("wrong secret with token still rejected", func(t *testing.T) { + iam := newTestIAMWithCreds() + req := mustNewRequest(http.MethodGet, "http://127.0.0.1:9000/bucket/object", 0, nil, t) + req.Header.Set("X-Amz-Security-Token", token) + require.NoError(t, signRequestV4(req, "access_key_1", "wrong_secret")) + + _, errCode := iam.reqSignatureV4Verify(req) + assert.Equal(t, s3err.ErrSignatureDoesNotMatch, errCode) + }) + + t.Run("presigned URL with token works", func(t *testing.T) { + iam := newTestIAMWithCreds() + req := mustNewRequest(http.MethodGet, "http://127.0.0.1:9000/bucket/object", 0, nil, t) + q := req.URL.Query() + q.Set("X-Amz-Security-Token", token) + req.URL.RawQuery = q.Encode() + require.NoError(t, preSignV4(iam, req, "access_key_1", "secret_key_1", int64((10*time.Minute).Seconds()))) + + _, _, errCode := iam.doesPresignedSignatureMatch(req) + require.Equal(t, s3err.ErrNone, errCode) + assert.True(t, s3_constants.IsSessionTokenIgnored(req.Context())) + }) +} + +func newTestIAMWithCreds() *IdentityAccessManagement { + iam := &IdentityAccessManagement{ + hashes: make(map[string]*sync.Pool), + hashCounters: make(map[string]*int32), + } + _ = iam.loadS3ApiConfiguration(&iam_pb.S3ApiConfiguration{ + Identities: []*iam_pb.Identity{ + { + Name: "someone", + Credentials: []*iam_pb.Credential{ + {AccessKey: "access_key_1", SecretKey: "secret_key_1"}, + }, + Actions: []string{"Admin", "Read", "Write"}, + }, + }, + }) + return iam +} diff --git a/weed/s3api/s3_constants/header.go b/weed/s3api/s3_constants/header.go index c8f788bcb..7f03adbb3 100644 --- a/weed/s3api/s3_constants/header.go +++ b/weed/s3api/s3_constants/header.go @@ -322,11 +322,12 @@ func IsSeaweedFSInternalHeader(headerKey string) bool { type contextKey string const ( - contextKeyIdentityName contextKey = "s3-identity-name" - contextKeyIdentityObject contextKey = "s3-identity-object" - contextKeyIdentityHolder contextKey = "s3-identity-holder" - contextKeyPrincipalArn contextKey = "s3-principal-arn" - contextKeyIdentityClaim contextKey = "s3-identity-claim" + contextKeyIdentityName contextKey = "s3-identity-name" + contextKeyIdentityObject contextKey = "s3-identity-object" + contextKeyIdentityHolder contextKey = "s3-identity-holder" + contextKeyPrincipalArn contextKey = "s3-principal-arn" + contextKeyIdentityClaim contextKey = "s3-identity-claim" + contextKeyIgnoredSessionToken contextKey = "s3-ignored-session-token" ) // identityHolder is a mutable container for the authenticated identity name, @@ -449,6 +450,22 @@ func GetIdentityClaimFromContext(r *http.Request) string { return "" } +// IgnoreSessionTokenInContext marks the request's session token as tolerated +// rather than authenticated: a statically configured access key signed the +// request, so the token (which credential vendors like Unity Catalog emit on +// static credentials too) must not steer authorization into the STS session +// path. A context value because headers can be spoofed; this cannot. +func IgnoreSessionTokenInContext(ctx context.Context) context.Context { + return context.WithValue(ctx, contextKeyIgnoredSessionToken, true) +} + +// IsSessionTokenIgnored reports whether the request carries a session token +// that was tolerated for a statically configured access key. +func IsSessionTokenIgnored(ctx context.Context) bool { + ignored, _ := ctx.Value(contextKeyIgnoredSessionToken).(bool) + return ignored +} + // SetIdentityInContext stores the full authenticated identity object in the request context // This is used to pass the full identity (including for JWT users) to handlers func SetIdentityInContext(ctx context.Context, identity interface{}) context.Context { diff --git a/weed/s3api/s3api_sts.go b/weed/s3api/s3api_sts.go index 813d34ba2..9daa1cacf 100644 --- a/weed/s3api/s3api_sts.go +++ b/weed/s3api/s3api_sts.go @@ -671,19 +671,6 @@ func (h *STSHandlers) handleGetFederationToken(w http.ResponseWriter, r *http.Re return } - // Reject calls from temporary credentials (session tokens) early, - // before SigV4 verification — no need to authenticate first. - // GetFederationToken can only be called by long-term IAM users. - securityToken := r.Header.Get("X-Amz-Security-Token") - if securityToken == "" { - securityToken = r.URL.Query().Get("X-Amz-Security-Token") - } - if securityToken != "" { - h.writeSTSErrorResponse(w, r, STSErrAccessDenied, - fmt.Errorf("GetFederationToken cannot be called with temporary credentials")) - return - } - // Check if STS service is initialized if h.stsService == nil || !h.stsService.IsInitialized() { h.writeSTSErrorResponse(w, r, STSErrSTSNotReady, @@ -713,6 +700,13 @@ func (h *STSHandlers) handleGetFederationToken(w http.ResponseWriter, r *http.Re return } + // GetFederationToken can only be called by long-term IAM users. + if extractSessionToken(r) != "" { + h.writeSTSErrorResponse(w, r, STSErrAccessDenied, + fmt.Errorf("GetFederationToken cannot be called with temporary credentials")) + return + } + r = r.WithContext(recordIdentityInContext(r, identity)) glog.V(2).Infof("GetFederationToken: caller identity=%s, name=%s", identity.Name, name) diff --git a/weed/s3api/s3api_sts_get_federation_token_test.go b/weed/s3api/s3api_sts_get_federation_token_test.go index 7375a1822..071c71613 100644 --- a/weed/s3api/s3api_sts_get_federation_token_test.go +++ b/weed/s3api/s3api_sts_get_federation_token_test.go @@ -9,6 +9,7 @@ import ( "net/url" "sort" "strings" + "sync" "testing" "time" @@ -129,34 +130,46 @@ func TestGetFederationToken_WithSessionPolicy(t *testing.T) { assert.Equal(t, []string{"S3FullAccess"}, sessionInfo.Policies) } -// TestGetFederationToken_RejectTemporaryCredentials tests that requests with -// session tokens are rejected. +// TestGetFederationToken_RejectTemporaryCredentials tests that requests +// authenticated with temporary credentials are rejected. func TestGetFederationToken_RejectTemporaryCredentials(t *testing.T) { stsService, _ := setupTestSTSService(t) - stsHandlers := NewSTSHandlers(stsService, &IdentityAccessManagement{ - iamIntegration: &MockIAMIntegration{}, - }) + iam := &IdentityAccessManagement{ + iamIntegration: &MockIAMIntegration{ + validateSessionFunc: func(context.Context, string) (*sts.SessionInfo, error) { + return &sts.SessionInfo{ + AssumedRoleUser: "role/user", + Principal: "arn:aws:sts:::assumed-role/role/user", + Credentials: &sts.Credentials{ + AccessKeyId: "STSACCESSKEY", + SecretAccessKey: "sts-secret-key", + }, + ExpiresAt: time.Now().Add(time.Hour), + }, nil + }, + }, + hashes: make(map[string]*sync.Pool), + hashCounters: make(map[string]*int32), + } + stsHandlers := NewSTSHandlers(stsService, iam) tests := []struct { - name string - setToken func(r *http.Request) - description string + name string + setToken func(r *http.Request) }{ { name: "SessionTokenInHeader", setToken: func(r *http.Request) { - r.Header.Set("X-Amz-Security-Token", "some-session-token") + r.Header.Set("X-Amz-Security-Token", "session-token") }, - description: "Session token in X-Amz-Security-Token header should be rejected", }, { name: "SessionTokenInQuery", setToken: func(r *http.Request) { q := r.URL.Query() - q.Set("X-Amz-Security-Token", "some-session-token") + q.Set("X-Amz-Security-Token", "session-token") r.URL.RawQuery = q.Encode() }, - description: "Session token in query string should be rejected", }, } @@ -167,28 +180,47 @@ func TestGetFederationToken_RejectTemporaryCredentials(t *testing.T) { form.Set("Name", "TestUser") form.Set("Version", "2011-06-15") - req := httptest.NewRequest("POST", "/", strings.NewReader(form.Encode())) + req := mustNewRequest(http.MethodPost, "http://sts.amazonaws.com/", + int64(len(form.Encode())), strings.NewReader(form.Encode()), t) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") tt.setToken(req) - - // Parse form so the handler can read it - require.NoError(t, req.ParseForm()) - // Re-set values after parse - req.Form.Set("Action", "GetFederationToken") - req.Form.Set("Name", "TestUser") - req.Form.Set("Version", "2011-06-15") + require.NoError(t, signRequestV4(req, "STSACCESSKEY", "sts-secret-key")) rr := httptest.NewRecorder() stsHandlers.HandleSTSRequest(rr, req) - // The handler rejects temporary credentials before SigV4 verification - assert.Equal(t, http.StatusForbidden, rr.Code, tt.description) + assert.Equal(t, http.StatusForbidden, rr.Code) assert.Contains(t, rr.Body.String(), "AccessDenied") assert.Contains(t, rr.Body.String(), "cannot be called with temporary credentials") }) } } +// A session token attached to a statically configured credential (as credential +// vendors emit) must not disqualify the caller as temporary. +func TestGetFederationToken_VendedStaticCredential(t *testing.T) { + stsService, _ := setupTestSTSService(t) + iam := newTestIAMWithCreds() + stsHandlers := NewSTSHandlers(stsService, iam) + + form := url.Values{} + form.Set("Action", "GetFederationToken") + form.Set("Name", "VendedApp") + form.Set("Version", "2011-06-15") + + req := mustNewRequest(http.MethodPost, "http://sts.amazonaws.com/", + int64(len(form.Encode())), strings.NewReader(form.Encode()), t) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("X-Amz-Security-Token", "vendor-token") + require.NoError(t, signRequestV4(req, "access_key_1", "secret_key_1")) + + rr := httptest.NewRecorder() + stsHandlers.HandleSTSRequest(rr, req) + + assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + assert.Contains(t, rr.Body.String(), "GetFederationTokenResponse") +} + // TestGetFederationToken_MissingName tests that a missing Name parameter returns an error func TestGetFederationToken_MissingName(t *testing.T) { stsService, _ := setupTestSTSService(t) diff --git a/weed/s3api/s3tables/iam.go b/weed/s3api/s3tables/iam.go index c5cbef59d..944d87ca8 100644 --- a/weed/s3api/s3tables/iam.go +++ b/weed/s3api/s3tables/iam.go @@ -97,6 +97,9 @@ func hasSessionToken(r *http.Request) bool { } func extractSessionToken(r *http.Request) string { + if s3_constants.IsSessionTokenIgnored(r.Context()) { + return "" + } if token := r.Header.Get(s3_constants.SeaweedFSSessionTokenHeader); token != "" { return token }