mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-20 15:04:37 +00:00
fix oidc
This commit is contained in:
@@ -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": [
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user