This commit is contained in:
chrislu
2025-08-25 14:16:54 -07:00
parent 2c30968b2e
commit 9c587dbd51
8 changed files with 470 additions and 30 deletions
+93 -1
View File
@@ -5,13 +5,51 @@
"issuer": "seaweedfs-sts",
"signingKey": "dGVzdC1zaWduaW5nLWtleS0zMi1jaGFyYWN0ZXJzLWxvbmc="
},
"identityProviders": [
"providers": [
{
"name": "test-oidc",
"type": "mock",
"config": {
"issuer": "test-oidc-issuer"
}
},
{
"name": "keycloak",
"type": "oidc",
"enabled": true,
"config": {
"issuer": "http://localhost:8080/realms/seaweedfs-test",
"clientId": "seaweedfs-s3",
"clientSecret": "seaweedfs-s3-secret",
"jwksUri": "http://localhost:8080/realms/seaweedfs-test/protocol/openid-connect/certs",
"userInfoUri": "http://localhost:8080/realms/seaweedfs-test/protocol/openid-connect/userinfo",
"scopes": ["openid", "profile", "email"],
"claimsMapping": {
"username": "preferred_username",
"email": "email",
"name": "name"
},
"roleMapping": {
"rules": [
{
"claim": "roles",
"value": "s3-admin",
"role": "arn:seaweed:iam::role/KeycloakAdminRole"
},
{
"claim": "roles",
"value": "s3-read-only",
"role": "arn:seaweed:iam::role/KeycloakReadOnlyRole"
},
{
"claim": "roles",
"value": "s3-write-only",
"role": "arn:seaweed:iam::role/KeycloakWriteOnlyRole"
}
],
"defaultRole": "arn:seaweed:iam::role/KeycloakReadOnlyRole"
}
}
}
],
"policy": {
@@ -72,6 +110,60 @@
},
"attachedPolicies": ["S3WriteOnlyPolicy"],
"description": "Write-only role for testing"
},
{
"roleName": "KeycloakAdminRole",
"roleArn": "arn:seaweed:iam::role/KeycloakAdminRole",
"trustPolicy": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "keycloak"
},
"Action": ["sts:AssumeRoleWithWebIdentity"]
}
]
},
"attachedPolicies": ["S3AdminPolicy"],
"description": "Admin role for Keycloak users"
},
{
"roleName": "KeycloakReadOnlyRole",
"roleArn": "arn:seaweed:iam::role/KeycloakReadOnlyRole",
"trustPolicy": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "keycloak"
},
"Action": ["sts:AssumeRoleWithWebIdentity"]
}
]
},
"attachedPolicies": ["S3ReadOnlyPolicy"],
"description": "Read-only role for Keycloak users"
},
{
"roleName": "KeycloakWriteOnlyRole",
"roleArn": "arn:seaweed:iam::role/KeycloakWriteOnlyRole",
"trustPolicy": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "keycloak"
},
"Action": ["sts:AssumeRoleWithWebIdentity"]
}
]
},
"attachedPolicies": ["S3WriteOnlyPolicy"],
"description": "Write-only role for Keycloak users"
}
],
"policies": [
+104 -6
View File
@@ -114,9 +114,10 @@ func NewKeycloakClient(baseURL, realm, clientID, clientSecret string) *KeycloakC
// isKeycloakAvailable checks if Keycloak is running and accessible
func (f *S3IAMTestFramework) isKeycloakAvailable(keycloakURL string) bool {
client := &http.Client{Timeout: 5 * time.Second}
healthURL := fmt.Sprintf("%s/health/ready", keycloakURL)
// Use realms endpoint instead of health/ready for Keycloak v26+
realmsURL := fmt.Sprintf("%s/realms/master", keycloakURL)
resp, err := client.Get(healthURL)
resp, err := client.Get(realmsURL)
if err != nil {
return false
}
@@ -268,16 +269,40 @@ func (t *BearerTokenTransport) RoundTrip(req *http.Request) (*http.Response, err
// Clone the request to avoid modifying the original
newReq := req.Clone(req.Context())
// Add Bearer token authorization header
newReq.Header.Set("Authorization", "Bearer "+t.Token)
// Remove AWS signature headers if present (they conflict with Bearer auth)
// Remove ALL existing Authorization headers first to prevent conflicts
newReq.Header.Del("Authorization")
newReq.Header.Del("X-Amz-Date")
newReq.Header.Del("X-Amz-Content-Sha256")
newReq.Header.Del("X-Amz-Signature")
newReq.Header.Del("X-Amz-Algorithm")
newReq.Header.Del("X-Amz-Credential")
newReq.Header.Del("X-Amz-SignedHeaders")
newReq.Header.Del("X-Amz-Security-Token")
// Add Bearer token authorization header
newReq.Header.Set("Authorization", "Bearer "+t.Token)
// Debug: log the token being sent (first 50 chars) and subject
tokenPreview := t.Token
if len(tokenPreview) > 50 {
tokenPreview = tokenPreview[:50] + "..."
}
// Debug logging can be enabled if needed
// Extract subject from token for debugging
// parts := strings.Split(t.Token, ".")
// subject := "unknown"
// if len(parts) >= 2 {
// if decoded, err := base64.RawURLEncoding.DecodeString(parts[1]); err == nil {
// var claims map[string]interface{}
// if json.Unmarshal(decoded, &claims) == nil {
// if sub, ok := claims["sub"].(string); ok {
// subject = sub[:8] + "..." // First 8 chars of subject
// }
// }
// }
// }
// fmt.Printf("DEBUG: Sending Bearer token (subject: %s): %s\n", subject, tokenPreview)
// Use underlying transport
transport := t.Transport
@@ -464,6 +489,79 @@ func (f *S3IAMTestFramework) CreateS3ClientWithSessionToken(sessionToken string)
return s3.New(sess), nil
}
// CreateS3ClientWithKeycloakToken creates an S3 client using a Keycloak JWT token
func (f *S3IAMTestFramework) CreateS3ClientWithKeycloakToken(keycloakToken string) (*s3.S3, error) {
// Create a fresh HTTP transport with aggressive timeouts to prevent hanging
transport := &http.Transport{
DisableKeepAlives: true, // Force new connections for each request
DisableCompression: true, // Disable compression to simplify requests
MaxIdleConns: 0, // No connection pooling
MaxIdleConnsPerHost: 0, // No connection pooling per host
IdleConnTimeout: 1 * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
// Create a custom HTTP client with aggressive timeouts
httpClient := &http.Client{
Timeout: 30 * time.Second, // Overall request timeout
Transport: &BearerTokenTransport{
Token: keycloakToken,
Transport: transport,
},
}
sess, err := session.NewSession(&aws.Config{
Region: aws.String(TestRegion),
Endpoint: aws.String(TestS3Endpoint),
Credentials: credentials.AnonymousCredentials,
DisableSSL: aws.Bool(true),
S3ForcePathStyle: aws.Bool(true),
HTTPClient: httpClient,
MaxRetries: aws.Int(0), // No retries to avoid delays
})
if err != nil {
return nil, fmt.Errorf("failed to create AWS session: %v", err)
}
return s3.New(sess), nil
}
// TestKeycloakTokenDirectly tests a Keycloak token with direct HTTP request (bypassing AWS SDK)
func (f *S3IAMTestFramework) TestKeycloakTokenDirectly(keycloakToken string) error {
// Create a simple HTTP client with timeout
client := &http.Client{
Timeout: 10 * time.Second,
}
// Create request to list buckets
req, err := http.NewRequest("GET", TestS3Endpoint, nil)
if err != nil {
return fmt.Errorf("failed to create request: %v", err)
}
// Add Bearer token
req.Header.Set("Authorization", "Bearer "+keycloakToken)
req.Header.Set("Host", "localhost:8333")
// Make request
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("request failed: %v", err)
}
defer resp.Body.Close()
// Read response
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed to read response: %v", err)
}
fmt.Printf("Direct HTTP test - Status: %d, Body: %s\n", resp.StatusCode, string(body))
return nil
}
// generateJWTToken creates a JWT token for testing
func (f *S3IAMTestFramework) generateJWTToken(username, roleName string, validDuration time.Duration) (string, error) {
now := time.Now()
+52 -14
View File
@@ -1,7 +1,10 @@
package iam
import (
"encoding/base64"
"encoding/json"
"os"
"strings"
"testing"
"github.com/aws/aws-sdk-go/service/s3"
@@ -43,7 +46,7 @@ func TestKeycloakAuthentication(t *testing.T) {
assert.NotEmpty(t, token, "JWT token should not be empty")
// Verify token can be used to create S3 client
s3Client, err := framework.CreateS3ClientWithJWT("admin-user", "S3AdminRole")
s3Client, err := framework.CreateS3ClientWithKeycloakToken(token)
require.NoError(t, err)
assert.NotNil(t, s3Client, "S3 client should be created successfully")
@@ -71,17 +74,42 @@ func TestKeycloakAuthentication(t *testing.T) {
require.NoError(t, err)
assert.NotEmpty(t, token, "JWT token should not be empty")
// Create S3 client with read-only user
s3Client, err := framework.CreateS3ClientWithJWT("read-user", "S3ReadOnlyRole")
require.NoError(t, err)
// Debug: decode token to verify it's for read-user
parts := strings.Split(token, ".")
if len(parts) >= 2 {
payload := parts[1]
// Add padding if needed
for len(payload)%4 != 0 {
payload += "="
}
decoded, err := base64.StdEncoding.DecodeString(payload)
if err == nil {
var claims map[string]interface{}
if json.Unmarshal(decoded, &claims) == nil {
t.Logf("Token username: %v", claims["preferred_username"])
t.Logf("Token roles: %v", claims["roles"])
}
}
}
// Test that read-only user can list buckets
_, err = s3Client.ListBuckets(&s3.ListBucketsInput{})
assert.NoError(t, err, "Read-only user should be able to list buckets")
// First test with direct HTTP request to verify OIDC authentication works
t.Logf("Testing with direct HTTP request...")
err = framework.TestKeycloakTokenDirectly(token)
require.NoError(t, err, "Direct HTTP test should succeed")
// Test that read-only user cannot create buckets
err = framework.CreateBucket(s3Client, testKeycloakBucket+"-readonly")
assert.Error(t, err, "Read-only user should not be able to create buckets")
// Create S3 client with Keycloak token
s3Client, err := framework.CreateS3ClientWithKeycloakToken(token)
require.NoError(t, err)
// Test that read-only user can list buckets
t.Logf("Testing ListBuckets with AWS SDK...")
_, err = s3Client.ListBuckets(&s3.ListBucketsInput{})
assert.NoError(t, err, "Read-only user should be able to list buckets")
// Test that read-only user cannot create buckets
t.Logf("Testing CreateBucket with AWS SDK...")
err = framework.CreateBucket(s3Client, testKeycloakBucket+"-readonly")
assert.Error(t, err, "Read-only user should not be able to create buckets")
})
t.Run("invalid_user_authentication", func(t *testing.T) {
@@ -110,7 +138,10 @@ func TestKeycloakTokenExpiration(t *testing.T) {
assert.Greater(t, tokenResp.ExpiresIn, 0, "Token should have expiration time")
// Test that token works initially
s3Client, err := framework.CreateS3ClientWithJWT("admin-user", "S3AdminRole")
token, err := framework.getKeycloakToken("admin-user")
require.NoError(t, err)
s3Client, err := framework.CreateS3ClientWithKeycloakToken(token)
require.NoError(t, err)
_, err = s3Client.ListBuckets(&s3.ListBucketsInput{})
@@ -158,8 +189,12 @@ func TestKeycloakRoleMapping(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.username, func(t *testing.T) {
// Create S3 client for the user
s3Client, err := framework.CreateS3ClientWithJWT(tc.username, tc.expectedRole)
// Get Keycloak token for the user
token, err := framework.getKeycloakToken(tc.username)
require.NoError(t, err)
// Create S3 client with Keycloak token
s3Client, err := framework.CreateS3ClientWithKeycloakToken(token)
require.NoError(t, err, tc.description)
// Test list buckets permission
@@ -192,7 +227,10 @@ func TestKeycloakS3Operations(t *testing.T) {
}
// Use admin user for comprehensive testing
s3Client, err := framework.CreateS3ClientWithJWT("admin-user", "S3AdminRole")
token, err := framework.getKeycloakToken("admin-user")
require.NoError(t, err)
s3Client, err := framework.CreateS3ClientWithKeycloakToken(token)
require.NoError(t, err)
bucketName := testKeycloakBucket + "-operations"