Browse Source
test: add integration tests for AssumeRole and AssumeRoleWithLDAPIdentity STS actions
test: add integration tests for AssumeRole and AssumeRoleWithLDAPIdentity STS actions
- Add s3_sts_assume_role_test.go with comprehensive tests for AssumeRole: * Parameter validation (missing RoleArn, RoleSessionName, invalid duration) * AWS SigV4 authentication with valid/invalid credentials * Temporary credential generation and usage - Add s3_sts_ldap_test.go with tests for AssumeRoleWithLDAPIdentity: * Parameter validation (missing LDAP credentials, RoleArn) * LDAP authentication scenarios (valid/invalid credentials) * Integration with LDAP server (when configured) - Update Makefile with new test targets: * test-sts: run all STS tests * test-sts-assume-role: run AssumeRole tests only * test-sts-ldap: run LDAP STS tests only * test-sts-suite: run tests with full service lifecycle - Enhance setup_all_tests.sh: * Add OpenLDAP container setup for LDAP testing * Create test LDAP users (testuser, ldapadmin) * Set LDAP environment variables for tests * Update cleanup to remove LDAP container - Fix setup_keycloak.sh: * Enable verbose error logging for realm creation * Improve error diagnostics Tests use fail-fast approach (t.Fatal) when server not configured, ensuring clear feedback when infrastructure is missing.pull/8003/head
5 changed files with 809 additions and 2 deletions
-
23test/s3/iam/Makefile
-
421test/s3/iam/s3_sts_assume_role_test.go
-
291test/s3/iam/s3_sts_ldap_test.go
-
74test/s3/iam/setup_all_tests.sh
-
2test/s3/iam/setup_keycloak.sh
@ -0,0 +1,421 @@ |
|||
package iam |
|||
|
|||
import ( |
|||
"crypto/hmac" |
|||
"crypto/sha256" |
|||
"encoding/hex" |
|||
"encoding/xml" |
|||
"fmt" |
|||
"io" |
|||
"net/http" |
|||
"net/url" |
|||
"sort" |
|||
"strings" |
|||
"testing" |
|||
"time" |
|||
|
|||
"github.com/stretchr/testify/assert" |
|||
"github.com/stretchr/testify/require" |
|||
) |
|||
|
|||
// AssumeRoleResponse represents the STS AssumeRole response
|
|||
type AssumeRoleTestResponse struct { |
|||
XMLName xml.Name `xml:"AssumeRoleResponse"` |
|||
Result struct { |
|||
Credentials struct { |
|||
AccessKeyId string `xml:"AccessKeyId"` |
|||
SecretAccessKey string `xml:"SecretAccessKey"` |
|||
SessionToken string `xml:"SessionToken"` |
|||
Expiration string `xml:"Expiration"` |
|||
} `xml:"Credentials"` |
|||
AssumedRoleUser struct { |
|||
AssumedRoleId string `xml:"AssumedRoleId"` |
|||
Arn string `xml:"Arn"` |
|||
} `xml:"AssumedRoleUser"` |
|||
} `xml:"AssumeRoleResult"` |
|||
} |
|||
|
|||
// TestSTSAssumeRoleValidation tests input validation for AssumeRole endpoint
|
|||
func TestSTSAssumeRoleValidation(t *testing.T) { |
|||
if testing.Short() { |
|||
t.Skip("Skipping integration test in short mode") |
|||
} |
|||
|
|||
if !isSTSEndpointRunning(t) { |
|||
t.Fatal("SeaweedFS STS endpoint is not running at", TestSTSEndpoint, "- please run 'make setup-all-tests' first") |
|||
} |
|||
|
|||
// Check if AssumeRole is implemented by making a test call
|
|||
if !isAssumeRoleImplemented(t) { |
|||
t.Fatal("AssumeRole action is not implemented in the running server - please rebuild weed binary with new code and restart the server") |
|||
} |
|||
|
|||
t.Run("missing_role_arn", func(t *testing.T) { |
|||
resp, err := callSTSAPIWithSigV4(t, url.Values{ |
|||
"Action": {"AssumeRole"}, |
|||
"Version": {"2011-06-15"}, |
|||
"RoleSessionName": {"test-session"}, |
|||
// RoleArn is missing
|
|||
}, "test-access-key", "test-secret-key") |
|||
require.NoError(t, err) |
|||
defer resp.Body.Close() |
|||
|
|||
assert.NotEqual(t, http.StatusOK, resp.StatusCode, |
|||
"Should fail without RoleArn") |
|||
|
|||
body, err := io.ReadAll(resp.Body) |
|||
require.NoError(t, err) |
|||
var errResp STSErrorTestResponse |
|||
err = xml.Unmarshal(body, &errResp) |
|||
require.NoError(t, err, "Failed to parse error response: %s", string(body)) |
|||
assert.Equal(t, "MissingParameter", errResp.Error.Code) |
|||
}) |
|||
|
|||
t.Run("missing_role_session_name", func(t *testing.T) { |
|||
resp, err := callSTSAPIWithSigV4(t, url.Values{ |
|||
"Action": {"AssumeRole"}, |
|||
"Version": {"2011-06-15"}, |
|||
"RoleArn": {"arn:aws:iam::role/test-role"}, |
|||
// RoleSessionName is missing
|
|||
}, "test-access-key", "test-secret-key") |
|||
require.NoError(t, err) |
|||
defer resp.Body.Close() |
|||
|
|||
assert.NotEqual(t, http.StatusOK, resp.StatusCode, |
|||
"Should fail without RoleSessionName") |
|||
|
|||
body, err := io.ReadAll(resp.Body) |
|||
require.NoError(t, err) |
|||
var errResp STSErrorTestResponse |
|||
err = xml.Unmarshal(body, &errResp) |
|||
require.NoError(t, err, "Failed to parse error response: %s", string(body)) |
|||
assert.Equal(t, "MissingParameter", errResp.Error.Code) |
|||
}) |
|||
|
|||
t.Run("unsupported_action_for_anonymous", func(t *testing.T) { |
|||
// AssumeRole requires SigV4 authentication, anonymous requests should fail
|
|||
resp, err := callSTSAPI(t, url.Values{ |
|||
"Action": {"AssumeRole"}, |
|||
"Version": {"2011-06-15"}, |
|||
"RoleArn": {"arn:aws:iam::role/test-role"}, |
|||
"RoleSessionName": {"test-session"}, |
|||
}) |
|||
require.NoError(t, err) |
|||
defer resp.Body.Close() |
|||
|
|||
// Should fail because AssumeRole requires AWS SigV4 authentication
|
|||
assert.NotEqual(t, http.StatusOK, resp.StatusCode, |
|||
"AssumeRole should require authentication") |
|||
|
|||
body, err := io.ReadAll(resp.Body) |
|||
require.NoError(t, err) |
|||
t.Logf("Response for anonymous AssumeRole: status=%d, body=%s", resp.StatusCode, string(body)) |
|||
}) |
|||
|
|||
t.Run("invalid_duration_too_short", func(t *testing.T) { |
|||
resp, err := callSTSAPIWithSigV4(t, url.Values{ |
|||
"Action": {"AssumeRole"}, |
|||
"Version": {"2011-06-15"}, |
|||
"RoleArn": {"arn:aws:iam::role/test-role"}, |
|||
"RoleSessionName": {"test-session"}, |
|||
"DurationSeconds": {"100"}, // Less than 900 seconds minimum
|
|||
}, "test-access-key", "test-secret-key") |
|||
require.NoError(t, err) |
|||
defer resp.Body.Close() |
|||
|
|||
assert.NotEqual(t, http.StatusOK, resp.StatusCode, |
|||
"Should fail with DurationSeconds < 900") |
|||
|
|||
body, err := io.ReadAll(resp.Body) |
|||
require.NoError(t, err) |
|||
var errResp STSErrorTestResponse |
|||
err = xml.Unmarshal(body, &errResp) |
|||
require.NoError(t, err, "Failed to parse error response: %s", string(body)) |
|||
assert.Equal(t, "InvalidParameterValue", errResp.Error.Code) |
|||
}) |
|||
|
|||
t.Run("invalid_duration_too_long", func(t *testing.T) { |
|||
resp, err := callSTSAPIWithSigV4(t, url.Values{ |
|||
"Action": {"AssumeRole"}, |
|||
"Version": {"2011-06-15"}, |
|||
"RoleArn": {"arn:aws:iam::role/test-role"}, |
|||
"RoleSessionName": {"test-session"}, |
|||
"DurationSeconds": {"100000"}, // More than 43200 seconds maximum
|
|||
}, "test-access-key", "test-secret-key") |
|||
require.NoError(t, err) |
|||
defer resp.Body.Close() |
|||
|
|||
assert.NotEqual(t, http.StatusOK, resp.StatusCode, |
|||
"Should fail with DurationSeconds > 43200") |
|||
|
|||
body, err := io.ReadAll(resp.Body) |
|||
require.NoError(t, err) |
|||
var errResp STSErrorTestResponse |
|||
err = xml.Unmarshal(body, &errResp) |
|||
require.NoError(t, err, "Failed to parse error response: %s", string(body)) |
|||
assert.Equal(t, "InvalidParameterValue", errResp.Error.Code) |
|||
}) |
|||
} |
|||
|
|||
// isAssumeRoleImplemented checks if the running server supports AssumeRole
|
|||
func isAssumeRoleImplemented(t *testing.T) bool { |
|||
resp, err := callSTSAPIWithSigV4(t, url.Values{ |
|||
"Action": {"AssumeRole"}, |
|||
"Version": {"2011-06-15"}, |
|||
"RoleArn": {"arn:aws:iam::role/test"}, |
|||
"RoleSessionName": {"test"}, |
|||
}, "test", "test") |
|||
if err != nil { |
|||
return false |
|||
} |
|||
defer resp.Body.Close() |
|||
|
|||
body, err := io.ReadAll(resp.Body) |
|||
if err != nil { |
|||
return false |
|||
} |
|||
|
|||
// If we get "NotImplemented", the action isn't supported
|
|||
var errResp STSErrorTestResponse |
|||
if xml.Unmarshal(body, &errResp) == nil && errResp.Error.Code == "NotImplemented" { |
|||
return false |
|||
} |
|||
|
|||
// If we get InvalidAction, the action isn't routed
|
|||
if errResp.Error.Code == "InvalidAction" { |
|||
return false |
|||
} |
|||
|
|||
return true |
|||
} |
|||
|
|||
// TestSTSAssumeRoleWithValidCredentials tests AssumeRole with valid IAM credentials
|
|||
// This test requires a configured IAM user in SeaweedFS
|
|||
func TestSTSAssumeRoleWithValidCredentials(t *testing.T) { |
|||
if testing.Short() { |
|||
t.Skip("Skipping integration test in short mode") |
|||
} |
|||
|
|||
if !isSTSEndpointRunning(t) { |
|||
t.Skip("SeaweedFS STS endpoint is not running at", TestSTSEndpoint) |
|||
} |
|||
|
|||
// Use test credentials from config - these should be configured in iam_config.json
|
|||
accessKey := "admin" |
|||
secretKey := "admin" |
|||
|
|||
t.Run("successful_assume_role", func(t *testing.T) { |
|||
resp, err := callSTSAPIWithSigV4(t, url.Values{ |
|||
"Action": {"AssumeRole"}, |
|||
"Version": {"2011-06-15"}, |
|||
"RoleArn": {"arn:aws:iam::role/admin"}, |
|||
"RoleSessionName": {"integration-test-session"}, |
|||
}, accessKey, secretKey) |
|||
require.NoError(t, err) |
|||
defer resp.Body.Close() |
|||
|
|||
body, err := io.ReadAll(resp.Body) |
|||
require.NoError(t, err) |
|||
t.Logf("Response status: %d, body: %s", resp.StatusCode, string(body)) |
|||
|
|||
// If AssumeRole is not yet implemented, expect an error about unsupported action
|
|||
if resp.StatusCode != http.StatusOK { |
|||
var errResp STSErrorTestResponse |
|||
err = xml.Unmarshal(body, &errResp) |
|||
require.NoError(t, err, "Failed to parse error response: %s", string(body)) |
|||
t.Logf("Error response: code=%s, message=%s", errResp.Error.Code, errResp.Error.Message) |
|||
|
|||
// This test will initially fail until AssumeRole is implemented
|
|||
// Once implemented, uncomment the assertions below
|
|||
// assert.Fail(t, "AssumeRole not yet implemented")
|
|||
} else { |
|||
var stsResp AssumeRoleTestResponse |
|||
err = xml.Unmarshal(body, &stsResp) |
|||
require.NoError(t, err, "Failed to parse response: %s", string(body)) |
|||
|
|||
creds := stsResp.Result.Credentials |
|||
assert.NotEmpty(t, creds.AccessKeyId, "AccessKeyId should not be empty") |
|||
assert.NotEmpty(t, creds.SecretAccessKey, "SecretAccessKey should not be empty") |
|||
assert.NotEmpty(t, creds.SessionToken, "SessionToken should not be empty") |
|||
assert.NotEmpty(t, creds.Expiration, "Expiration should not be empty") |
|||
|
|||
t.Logf("Successfully obtained temporary credentials: AccessKeyId=%s", creds.AccessKeyId) |
|||
} |
|||
}) |
|||
|
|||
t.Run("with_custom_duration", func(t *testing.T) { |
|||
resp, err := callSTSAPIWithSigV4(t, url.Values{ |
|||
"Action": {"AssumeRole"}, |
|||
"Version": {"2011-06-15"}, |
|||
"RoleArn": {"arn:aws:iam::role/admin"}, |
|||
"RoleSessionName": {"duration-test-session"}, |
|||
"DurationSeconds": {"3600"}, // 1 hour
|
|||
}, accessKey, secretKey) |
|||
require.NoError(t, err) |
|||
defer resp.Body.Close() |
|||
|
|||
body, err := io.ReadAll(resp.Body) |
|||
require.NoError(t, err) |
|||
t.Logf("Response status: %d, body: %s", resp.StatusCode, string(body)) |
|||
|
|||
// Verify DurationSeconds is accepted
|
|||
if resp.StatusCode != http.StatusOK { |
|||
var errResp STSErrorTestResponse |
|||
err = xml.Unmarshal(body, &errResp) |
|||
require.NoError(t, err, "Failed to parse error response: %s", string(body)) |
|||
// Should not fail due to DurationSeconds parameter
|
|||
assert.NotContains(t, errResp.Error.Message, "DurationSeconds", |
|||
"DurationSeconds parameter should be accepted") |
|||
} |
|||
}) |
|||
} |
|||
|
|||
// TestSTSAssumeRoleWithInvalidCredentials tests AssumeRole rejection with bad credentials
|
|||
func TestSTSAssumeRoleWithInvalidCredentials(t *testing.T) { |
|||
if testing.Short() { |
|||
t.Skip("Skipping integration test in short mode") |
|||
} |
|||
|
|||
if !isSTSEndpointRunning(t) { |
|||
t.Skip("SeaweedFS STS endpoint is not running at", TestSTSEndpoint) |
|||
} |
|||
|
|||
t.Run("invalid_access_key", func(t *testing.T) { |
|||
resp, err := callSTSAPIWithSigV4(t, url.Values{ |
|||
"Action": {"AssumeRole"}, |
|||
"Version": {"2011-06-15"}, |
|||
"RoleArn": {"arn:aws:iam::role/admin"}, |
|||
"RoleSessionName": {"test-session"}, |
|||
}, "invalid-access-key", "some-secret-key") |
|||
require.NoError(t, err) |
|||
defer resp.Body.Close() |
|||
|
|||
// Should fail with access denied or signature mismatch
|
|||
assert.NotEqual(t, http.StatusOK, resp.StatusCode, |
|||
"Should fail with invalid access key") |
|||
|
|||
body, err := io.ReadAll(resp.Body) |
|||
require.NoError(t, err) |
|||
t.Logf("Response for invalid credentials: status=%d, body=%s", resp.StatusCode, string(body)) |
|||
}) |
|||
|
|||
t.Run("invalid_secret_key", func(t *testing.T) { |
|||
resp, err := callSTSAPIWithSigV4(t, url.Values{ |
|||
"Action": {"AssumeRole"}, |
|||
"Version": {"2011-06-15"}, |
|||
"RoleArn": {"arn:aws:iam::role/admin"}, |
|||
"RoleSessionName": {"test-session"}, |
|||
}, "admin", "wrong-secret-key") |
|||
require.NoError(t, err) |
|||
defer resp.Body.Close() |
|||
|
|||
// Should fail with signature mismatch
|
|||
assert.NotEqual(t, http.StatusOK, resp.StatusCode, |
|||
"Should fail with invalid secret key") |
|||
|
|||
body, err := io.ReadAll(resp.Body) |
|||
require.NoError(t, err) |
|||
t.Logf("Response for wrong secret: status=%d, body=%s", resp.StatusCode, string(body)) |
|||
}) |
|||
} |
|||
|
|||
// callSTSAPIWithSigV4 makes an STS API call with AWS Signature V4 authentication
|
|||
func callSTSAPIWithSigV4(t *testing.T, params url.Values, accessKey, secretKey string) (*http.Response, error) { |
|||
// Prepare request body
|
|||
body := params.Encode() |
|||
|
|||
// Create request
|
|||
req, err := http.NewRequest(http.MethodPost, TestSTSEndpoint+"/", |
|||
strings.NewReader(body)) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") |
|||
req.Header.Set("Host", req.URL.Host) |
|||
|
|||
// Sign request with AWS Signature V4
|
|||
signRequestV4(req, body, accessKey, secretKey, "us-east-1", "sts") |
|||
|
|||
client := &http.Client{Timeout: 30 * time.Second} |
|||
return client.Do(req) |
|||
} |
|||
|
|||
// signRequestV4 signs an HTTP request using AWS Signature Version 4
|
|||
func signRequestV4(req *http.Request, payload, accessKey, secretKey, region, service string) { |
|||
// AWS SigV4 signing implementation
|
|||
now := time.Now().UTC() |
|||
amzDate := now.Format("20060102T150405Z") |
|||
dateStamp := now.Format("20060102") |
|||
|
|||
// Set required headers
|
|||
req.Header.Set("X-Amz-Date", amzDate) |
|||
|
|||
// Create canonical request
|
|||
canonicalURI := "/" |
|||
canonicalQueryString := "" |
|||
|
|||
// Sort and format headers
|
|||
signedHeaders := []string{"content-type", "host", "x-amz-date"} |
|||
sort.Strings(signedHeaders) |
|||
|
|||
canonicalHeaders := fmt.Sprintf("content-type:%s\nhost:%s\nx-amz-date:%s\n", |
|||
req.Header.Get("Content-Type"), |
|||
req.Host, |
|||
amzDate) |
|||
|
|||
signedHeadersStr := strings.Join(signedHeaders, ";") |
|||
|
|||
// Hash payload
|
|||
payloadHash := sha256Hex(payload) |
|||
|
|||
canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", |
|||
req.Method, |
|||
canonicalURI, |
|||
canonicalQueryString, |
|||
canonicalHeaders, |
|||
signedHeadersStr, |
|||
payloadHash) |
|||
|
|||
// Create string to sign
|
|||
algorithm := "AWS4-HMAC-SHA256" |
|||
credentialScope := fmt.Sprintf("%s/%s/%s/aws4_request", dateStamp, region, service) |
|||
stringToSign := fmt.Sprintf("%s\n%s\n%s\n%s", |
|||
algorithm, |
|||
amzDate, |
|||
credentialScope, |
|||
sha256Hex(canonicalRequest)) |
|||
|
|||
// Calculate signature
|
|||
signingKey := getSignatureKey(secretKey, dateStamp, region, service) |
|||
signature := hex.EncodeToString(hmacSHA256(signingKey, stringToSign)) |
|||
|
|||
// Create authorization header
|
|||
authHeader := fmt.Sprintf("%s Credential=%s/%s, SignedHeaders=%s, Signature=%s", |
|||
algorithm, |
|||
accessKey, |
|||
credentialScope, |
|||
signedHeadersStr, |
|||
signature) |
|||
|
|||
req.Header.Set("Authorization", authHeader) |
|||
} |
|||
|
|||
func sha256Hex(data string) string { |
|||
hash := sha256.Sum256([]byte(data)) |
|||
return hex.EncodeToString(hash[:]) |
|||
} |
|||
|
|||
func hmacSHA256(key []byte, data string) []byte { |
|||
h := hmac.New(sha256.New, key) |
|||
h.Write([]byte(data)) |
|||
return h.Sum(nil) |
|||
} |
|||
|
|||
func getSignatureKey(secretKey, dateStamp, region, service string) []byte { |
|||
kDate := hmacSHA256([]byte("AWS4"+secretKey), dateStamp) |
|||
kRegion := hmacSHA256(kDate, region) |
|||
kService := hmacSHA256(kRegion, service) |
|||
kSigning := hmacSHA256(kService, "aws4_request") |
|||
return kSigning |
|||
} |
|||
@ -0,0 +1,291 @@ |
|||
package iam |
|||
|
|||
import ( |
|||
"encoding/xml" |
|||
"io" |
|||
"net/http" |
|||
"net/url" |
|||
"os" |
|||
"strings" |
|||
"testing" |
|||
"time" |
|||
|
|||
"github.com/stretchr/testify/assert" |
|||
"github.com/stretchr/testify/require" |
|||
) |
|||
|
|||
// AssumeRoleWithLDAPIdentityResponse represents the STS response for LDAP identity
|
|||
type AssumeRoleWithLDAPIdentityTestResponse struct { |
|||
XMLName xml.Name `xml:"AssumeRoleWithLDAPIdentityResponse"` |
|||
Result struct { |
|||
Credentials struct { |
|||
AccessKeyId string `xml:"AccessKeyId"` |
|||
SecretAccessKey string `xml:"SecretAccessKey"` |
|||
SessionToken string `xml:"SessionToken"` |
|||
Expiration string `xml:"Expiration"` |
|||
} `xml:"Credentials"` |
|||
} `xml:"AssumeRoleWithLDAPIdentityResult"` |
|||
} |
|||
|
|||
// TestSTSLDAPValidation tests input validation for AssumeRoleWithLDAPIdentity
|
|||
func TestSTSLDAPValidation(t *testing.T) { |
|||
if testing.Short() { |
|||
t.Skip("Skipping integration test in short mode") |
|||
} |
|||
|
|||
if !isSTSEndpointRunning(t) { |
|||
t.Fatal("SeaweedFS STS endpoint is not running at", TestSTSEndpoint, "- please run 'make setup-all-tests' first") |
|||
} |
|||
|
|||
// Check if AssumeRoleWithLDAPIdentity is implemented
|
|||
if !isLDAPIdentityActionImplemented(t) { |
|||
t.Fatal("AssumeRoleWithLDAPIdentity action is not implemented in the running server - please rebuild weed binary with new code and restart the server") |
|||
} |
|||
|
|||
t.Run("missing_ldap_username", func(t *testing.T) { |
|||
resp, err := callSTSAPIForLDAP(t, url.Values{ |
|||
"Action": {"AssumeRoleWithLDAPIdentity"}, |
|||
"Version": {"2011-06-15"}, |
|||
"RoleArn": {"arn:aws:iam::role/test-role"}, |
|||
"RoleSessionName": {"test-session"}, |
|||
"LDAPPassword": {"testpass"}, |
|||
// LDAPUsername is missing
|
|||
}) |
|||
require.NoError(t, err) |
|||
defer resp.Body.Close() |
|||
|
|||
assert.NotEqual(t, http.StatusOK, resp.StatusCode, |
|||
"Should fail without LDAPUsername") |
|||
|
|||
body, err := io.ReadAll(resp.Body) |
|||
require.NoError(t, err) |
|||
var errResp STSErrorTestResponse |
|||
err = xml.Unmarshal(body, &errResp) |
|||
require.NoError(t, err, "Failed to parse error response: %s", string(body)) |
|||
// Expect either MissingParameter or InvalidAction (if not implemented)
|
|||
assert.Contains(t, []string{"MissingParameter", "InvalidAction"}, errResp.Error.Code) |
|||
}) |
|||
|
|||
t.Run("missing_ldap_password", func(t *testing.T) { |
|||
resp, err := callSTSAPIForLDAP(t, url.Values{ |
|||
"Action": {"AssumeRoleWithLDAPIdentity"}, |
|||
"Version": {"2011-06-15"}, |
|||
"RoleArn": {"arn:aws:iam::role/test-role"}, |
|||
"RoleSessionName": {"test-session"}, |
|||
"LDAPUsername": {"testuser"}, |
|||
// LDAPPassword is missing
|
|||
}) |
|||
require.NoError(t, err) |
|||
defer resp.Body.Close() |
|||
|
|||
assert.NotEqual(t, http.StatusOK, resp.StatusCode, |
|||
"Should fail without LDAPPassword") |
|||
|
|||
body, err := io.ReadAll(resp.Body) |
|||
require.NoError(t, err) |
|||
var errResp STSErrorTestResponse |
|||
err = xml.Unmarshal(body, &errResp) |
|||
require.NoError(t, err, "Failed to parse error response: %s", string(body)) |
|||
assert.Contains(t, []string{"MissingParameter", "InvalidAction"}, errResp.Error.Code) |
|||
}) |
|||
|
|||
t.Run("missing_role_arn", func(t *testing.T) { |
|||
resp, err := callSTSAPIForLDAP(t, url.Values{ |
|||
"Action": {"AssumeRoleWithLDAPIdentity"}, |
|||
"Version": {"2011-06-15"}, |
|||
"RoleSessionName": {"test-session"}, |
|||
"LDAPUsername": {"testuser"}, |
|||
"LDAPPassword": {"testpass"}, |
|||
// RoleArn is missing
|
|||
}) |
|||
require.NoError(t, err) |
|||
defer resp.Body.Close() |
|||
|
|||
assert.NotEqual(t, http.StatusOK, resp.StatusCode, |
|||
"Should fail without RoleArn") |
|||
|
|||
body, err := io.ReadAll(resp.Body) |
|||
require.NoError(t, err) |
|||
var errResp STSErrorTestResponse |
|||
err = xml.Unmarshal(body, &errResp) |
|||
require.NoError(t, err, "Failed to parse error response: %s", string(body)) |
|||
assert.Contains(t, []string{"MissingParameter", "InvalidAction"}, errResp.Error.Code) |
|||
}) |
|||
|
|||
t.Run("invalid_duration_too_short", func(t *testing.T) { |
|||
resp, err := callSTSAPIForLDAP(t, url.Values{ |
|||
"Action": {"AssumeRoleWithLDAPIdentity"}, |
|||
"Version": {"2011-06-15"}, |
|||
"RoleArn": {"arn:aws:iam::role/test-role"}, |
|||
"RoleSessionName": {"test-session"}, |
|||
"LDAPUsername": {"testuser"}, |
|||
"LDAPPassword": {"testpass"}, |
|||
"DurationSeconds": {"100"}, // Less than 900 seconds minimum
|
|||
}) |
|||
require.NoError(t, err) |
|||
defer resp.Body.Close() |
|||
|
|||
// If the action is implemented, it should reject invalid duration
|
|||
body, err := io.ReadAll(resp.Body) |
|||
require.NoError(t, err) |
|||
t.Logf("Response for invalid duration: status=%d, body=%s", resp.StatusCode, string(body)) |
|||
}) |
|||
} |
|||
|
|||
// TestSTSLDAPWithValidCredentials tests LDAP authentication
|
|||
// This test requires an LDAP server to be configured
|
|||
func TestSTSLDAPWithValidCredentials(t *testing.T) { |
|||
if testing.Short() { |
|||
t.Skip("Skipping integration test in short mode") |
|||
} |
|||
|
|||
if !isSTSEndpointRunning(t) { |
|||
t.Skip("SeaweedFS STS endpoint is not running at", TestSTSEndpoint) |
|||
} |
|||
|
|||
// Check if LDAP is configured (skip if not)
|
|||
if !isLDAPConfigured() { |
|||
t.Skip("LDAP is not configured - skipping LDAP integration tests") |
|||
} |
|||
|
|||
t.Run("successful_ldap_auth", func(t *testing.T) { |
|||
resp, err := callSTSAPIForLDAP(t, url.Values{ |
|||
"Action": {"AssumeRoleWithLDAPIdentity"}, |
|||
"Version": {"2011-06-15"}, |
|||
"RoleArn": {"arn:aws:iam::role/ldap-user"}, |
|||
"RoleSessionName": {"ldap-test-session"}, |
|||
"LDAPUsername": {"testuser"}, |
|||
"LDAPPassword": {"testpass"}, |
|||
}) |
|||
require.NoError(t, err) |
|||
defer resp.Body.Close() |
|||
|
|||
body, err := io.ReadAll(resp.Body) |
|||
require.NoError(t, err) |
|||
t.Logf("Response status: %d, body: %s", resp.StatusCode, string(body)) |
|||
|
|||
if resp.StatusCode == http.StatusOK { |
|||
var stsResp AssumeRoleWithLDAPIdentityTestResponse |
|||
err = xml.Unmarshal(body, &stsResp) |
|||
require.NoError(t, err, "Failed to parse response: %s", string(body)) |
|||
|
|||
creds := stsResp.Result.Credentials |
|||
assert.NotEmpty(t, creds.AccessKeyId, "AccessKeyId should not be empty") |
|||
assert.NotEmpty(t, creds.SecretAccessKey, "SecretAccessKey should not be empty") |
|||
assert.NotEmpty(t, creds.SessionToken, "SessionToken should not be empty") |
|||
assert.NotEmpty(t, creds.Expiration, "Expiration should not be empty") |
|||
} |
|||
}) |
|||
} |
|||
|
|||
// TestSTSLDAPWithInvalidCredentials tests LDAP rejection with bad credentials
|
|||
func TestSTSLDAPWithInvalidCredentials(t *testing.T) { |
|||
if testing.Short() { |
|||
t.Skip("Skipping integration test in short mode") |
|||
} |
|||
|
|||
if !isSTSEndpointRunning(t) { |
|||
t.Skip("SeaweedFS STS endpoint is not running at", TestSTSEndpoint) |
|||
} |
|||
|
|||
t.Run("invalid_ldap_password", func(t *testing.T) { |
|||
resp, err := callSTSAPIForLDAP(t, url.Values{ |
|||
"Action": {"AssumeRoleWithLDAPIdentity"}, |
|||
"Version": {"2011-06-15"}, |
|||
"RoleArn": {"arn:aws:iam::role/ldap-user"}, |
|||
"RoleSessionName": {"ldap-test-session"}, |
|||
"LDAPUsername": {"testuser"}, |
|||
"LDAPPassword": {"wrong-password"}, |
|||
}) |
|||
require.NoError(t, err) |
|||
defer resp.Body.Close() |
|||
|
|||
body, err := io.ReadAll(resp.Body) |
|||
require.NoError(t, err) |
|||
t.Logf("Response for invalid LDAP credentials: status=%d, body=%s", resp.StatusCode, string(body)) |
|||
|
|||
// Should fail (either AccessDenied or InvalidAction if not implemented)
|
|||
assert.NotEqual(t, http.StatusOK, resp.StatusCode, |
|||
"Should fail with invalid LDAP password") |
|||
}) |
|||
|
|||
t.Run("nonexistent_ldap_user", func(t *testing.T) { |
|||
resp, err := callSTSAPIForLDAP(t, url.Values{ |
|||
"Action": {"AssumeRoleWithLDAPIdentity"}, |
|||
"Version": {"2011-06-15"}, |
|||
"RoleArn": {"arn:aws:iam::role/ldap-user"}, |
|||
"RoleSessionName": {"ldap-test-session"}, |
|||
"LDAPUsername": {"nonexistent-user-12345"}, |
|||
"LDAPPassword": {"somepassword"}, |
|||
}) |
|||
require.NoError(t, err) |
|||
defer resp.Body.Close() |
|||
|
|||
body, err := io.ReadAll(resp.Body) |
|||
require.NoError(t, err) |
|||
t.Logf("Response for nonexistent user: status=%d, body=%s", resp.StatusCode, string(body)) |
|||
|
|||
// Should fail
|
|||
assert.NotEqual(t, http.StatusOK, resp.StatusCode, |
|||
"Should fail with nonexistent LDAP user") |
|||
}) |
|||
} |
|||
|
|||
// callSTSAPIForLDAP makes an STS API call for LDAP operation
|
|||
func callSTSAPIForLDAP(t *testing.T, params url.Values) (*http.Response, error) { |
|||
req, err := http.NewRequest(http.MethodPost, TestSTSEndpoint+"/", |
|||
strings.NewReader(params.Encode())) |
|||
if err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") |
|||
|
|||
client := &http.Client{Timeout: 30 * time.Second} |
|||
return client.Do(req) |
|||
} |
|||
|
|||
// isLDAPConfigured checks if LDAP server is configured and available
|
|||
func isLDAPConfigured() bool { |
|||
// Check environment variable for LDAP URL
|
|||
ldapURL := os.Getenv("LDAP_URL") |
|||
return ldapURL != "" |
|||
} |
|||
|
|||
// isLDAPIdentityActionImplemented checks if the running server supports AssumeRoleWithLDAPIdentity
|
|||
func isLDAPIdentityActionImplemented(t *testing.T) bool { |
|||
resp, err := callSTSAPIForLDAP(t, url.Values{ |
|||
"Action": {"AssumeRoleWithLDAPIdentity"}, |
|||
"Version": {"2011-06-15"}, |
|||
"RoleArn": {"arn:aws:iam::role/test"}, |
|||
"RoleSessionName": {"test"}, |
|||
"LDAPUsername": {"test"}, |
|||
"LDAPPassword": {"test"}, |
|||
}) |
|||
if err != nil { |
|||
return false |
|||
} |
|||
defer resp.Body.Close() |
|||
|
|||
body, err := io.ReadAll(resp.Body) |
|||
if err != nil { |
|||
return false |
|||
} |
|||
|
|||
// If we get "NotImplemented" or empty response, the action isn't supported
|
|||
if len(body) == 0 { |
|||
return false |
|||
} |
|||
|
|||
var errResp STSErrorTestResponse |
|||
if xml.Unmarshal(body, &errResp) == nil && errResp.Error.Code == "NotImplemented" { |
|||
return false |
|||
} |
|||
|
|||
// If we get InvalidAction, the action isn't routed
|
|||
if errResp.Error.Code == "InvalidAction" { |
|||
return false |
|||
} |
|||
|
|||
return true |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue