mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-14 02:20:41 +02:00
* iceberg: vend table-scoped credentials to clients that ask for delegation The catalog recognised X-Iceberg-Access-Delegation: vended-credentials and then deliberately said nothing, because it had nothing to vend: it withheld even the S3 endpoint so the client would keep the credentials it was configured with. That left every engine expecting the catalog to hand out access - Snowflake, Databricks, Trino with vending, any multi-tenant setup - needing static S3 keys distributed out of band. Mint an STS session per request instead, scoped by a session policy to the table's own prefix plus the bucket listing needed to resolve it, and return it in the load response config and storage-credentials. The role to assume is named by -s3.iceberg.credentialRole; its trust policy is what decides whether a caller may assume it, and vending stays off until it is set. A failed mint falls back to the old silence rather than handing back an endpoint the client cannot sign for. * iceberg: keep vended credentials inside the table prefix Review follow-ups on credential vending: Listing was granted on the bucket ARN with no condition, so a credential vended for one table could enumerate every other table's object names. Constrain s3:prefix to the table's own prefix, which the S3 gateway already populates for list requests. A table location carrying * or ? would have gone into the policy's resource pattern unescaped and widened the session to sibling prefixes. Refuse to vend for such a location rather than escaping it; nothing the catalog generates contains those characters. DurationSeconds skipped the 900..43200 bounds the other assume-role paths enforce, so -s3.iceberg.credentialDurationSeconds could ask for a session outside them. The check is now shared by all three entry points. * iceberg: return the vended credentials from buildFileIOConfig itself buildStorageConfig was a second name for what buildFileIOConfig already did; it now returns the storage credentials alongside the properties, and callers that only want the properties drop them. * iceberg: split the vended bucket grants, and refuse a whole-bucket scope The prefix condition sat on a statement that also granted GetBucketLocation and ListBucketMultipartUploads, neither of which carries an s3:prefix to satisfy it, so both were denied for every vended credential. GetBucketLocation moves to its own unconditioned statement. ListBucketMultipartUploads is dropped: Iceberg writers complete and abort by upload id, and granting it either leaks in-flight keys bucket-wide or breaks on the same missing prefix. A table whose location has no prefix - one registered at the bucket root - would have been vended read and write over every other table in the bucket. Refuse, the way a location with wildcards is refused.
101 lines
3.4 KiB
Go
101 lines
3.4 KiB
Go
package sts
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// TestTemporaryCredentialPrefix verifies that temporary credentials use ASIA prefix
|
|
// (not AKIA which is for permanent IAM user credentials)
|
|
func TestTemporaryCredentialPrefix(t *testing.T) {
|
|
sessionId := "test-session-for-prefix"
|
|
expiration := time.Now().Add(time.Hour)
|
|
|
|
credGen := NewCredentialGenerator()
|
|
cred, err := credGen.GenerateTemporaryCredentials(sessionId, expiration)
|
|
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, cred)
|
|
|
|
// Verify ASIA prefix for temporary credentials
|
|
assert.True(t, strings.HasPrefix(cred.AccessKeyId, "ASIA"),
|
|
"Temporary credentials must use ASIA prefix, got: %s", cred.AccessKeyId)
|
|
|
|
// Verify it's NOT using AKIA (permanent credentials)
|
|
assert.False(t, strings.HasPrefix(cred.AccessKeyId, "AKIA"),
|
|
"Temporary credentials must NOT use AKIA prefix (that's for permanent IAM keys)")
|
|
}
|
|
|
|
// TestTemporaryCredentialFormat verifies the full format of temporary credentials
|
|
func TestTemporaryCredentialFormat(t *testing.T) {
|
|
sessionId := "format-test-session"
|
|
expiration := time.Now().Add(time.Hour)
|
|
|
|
credGen := NewCredentialGenerator()
|
|
cred, err := credGen.GenerateTemporaryCredentials(sessionId, expiration)
|
|
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, cred)
|
|
|
|
// AWS temporary access key format: ASIA + 16 hex characters = 20 chars total
|
|
assert.Equal(t, 20, len(cred.AccessKeyId),
|
|
"Access key ID should be 20 characters (ASIA + 16 hex chars)")
|
|
|
|
// Verify it starts with ASIA
|
|
assert.True(t, strings.HasPrefix(cred.AccessKeyId, "ASIA"),
|
|
"Access key must start with ASIA prefix")
|
|
|
|
// Verify the rest is hex (after ASIA prefix)
|
|
hexPart := cred.AccessKeyId[4:]
|
|
assert.Equal(t, 16, len(hexPart), "Hex part should be 16 characters")
|
|
_, err = hex.DecodeString(hexPart)
|
|
assert.NoError(t, err, "The part after ASIA prefix should be valid hex")
|
|
|
|
// Verify secret key is not empty and is a valid base64-encoded SHA256 hash
|
|
assert.NotEmpty(t, cred.SecretAccessKey)
|
|
assert.Equal(t, 44, len(cred.SecretAccessKey),
|
|
"SecretAccessKey should be 44 characters for a base64-encoded 32-byte hash")
|
|
_, err = base64.StdEncoding.DecodeString(cred.SecretAccessKey)
|
|
assert.NoError(t, err, "SecretAccessKey should be a valid base64 string")
|
|
|
|
// Verify session token is not empty
|
|
assert.NotEmpty(t, cred.SessionToken)
|
|
}
|
|
|
|
// A duration that came from configuration is bounded the same as one from a
|
|
// request: the catalog passes -s3.iceberg.credentialDurationSeconds straight
|
|
// through.
|
|
func TestAssumeRoleForPrincipalValidatesDuration(t *testing.T) {
|
|
svc := NewSTSService()
|
|
err := svc.Initialize(&STSConfig{
|
|
TokenDuration: FlexibleDuration{time.Hour},
|
|
MaxSessionLength: FlexibleDuration{12 * time.Hour},
|
|
Issuer: "test-issuer",
|
|
SigningKey: []byte("test-signing-key-at-least-32-bytes-long"),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Initialize() error = %v", err)
|
|
}
|
|
|
|
for _, seconds := range []int64{60, 100000} {
|
|
duration := seconds
|
|
_, err := svc.AssumeRoleForPrincipal(context.Background(), &AssumeRoleForPrincipalRequest{
|
|
RoleArn: "arn:aws:iam::role/IcebergTableAccess",
|
|
Principal: "admin",
|
|
DurationSeconds: &duration,
|
|
})
|
|
if err == nil {
|
|
t.Fatalf("AssumeRoleForPrincipal(%ds) error = nil, want a validation error", seconds)
|
|
}
|
|
if !strings.Contains(err.Error(), "DurationSeconds") {
|
|
t.Errorf("AssumeRoleForPrincipal(%ds) error = %v, want it to name DurationSeconds", seconds, err)
|
|
}
|
|
}
|
|
}
|