Browse Source

fix: address PR review security issues for STS handlers

This commit addresses all critical security issues from PR review:

Security Fixes:
- Use crypto/rand for cryptographically secure credential generation
  instead of time.Now().UnixNano() (fixes predictable credentials)
- Add sts:AssumeRole permission check via VerifyActionPermission to
  prevent unauthorized role assumption
- Generate proper session tokens using crypto/rand instead of
  placeholder strings

Code Quality Improvements:
- Refactor DurationSeconds parsing into reusable parseDurationSeconds()
  helper function used by all three STS handlers
- Create generateSecureCredentials() helper for consistent and secure
  temporary credential generation
- Fix iamMatcher to check query string as fallback when Action not
  found in form data

LDAP Provider Implementation:
- Add go-ldap/ldap/v3 dependency
- Create LDAPProvider implementing IdentityProvider interface with
  full LDAP authentication support (connect, bind, search, groups)
- Update ProviderFactory to create real LDAP providers
- Wire LDAP provider into AssumeRoleWithLDAPIdentity handler

Test Infrastructure:
- Add LDAP user creation verification step in setup_all_tests.sh
pull/8003/head
Chris Lu 1 day ago
parent
commit
e34ea6b54a
  1. 2
      go.mod
  2. 4
      go.sum
  3. 8
      test/s3/iam/setup_all_tests.sh
  4. 428
      weed/iam/ldap/ldap_provider.go
  5. 8
      weed/iam/sts/provider_factory.go
  6. 20
      weed/s3api/s3api_server.go
  7. 254
      weed/s3api/s3api_sts.go

2
go.mod

@ -183,6 +183,8 @@ require (
github.com/cockroachdb/redact v1.1.5 // indirect
github.com/cockroachdb/version v0.0.0-20250314144055-3860cd14adf2 // indirect
github.com/dave/dst v0.27.2 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-ldap/ldap/v3 v3.4.12 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
github.com/google/go-cmp v0.7.0 // indirect

4
go.sum

@ -936,6 +936,8 @@ github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-darwin/apfs v0.0.0-20211011131704-f84b94dbf348 h1:JnrjqG5iR07/8k7NqrLNilRsl3s1EPRQEGvbPyOce68=
@ -957,6 +959,8 @@ github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk=
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=

8
test/s3/iam/setup_all_tests.sh

@ -109,6 +109,14 @@ uid: ldapadmin
userPassword: ldapadminpass
EOF
# Verify test users were created successfully
echo -e "${YELLOW}🔍 Verifying LDAP test users...${NC}"
if docker exec openldap-iam-test ldapsearch -x -D "cn=admin,dc=seaweedfs,dc=test" -w adminpassword -b "ou=users,dc=seaweedfs,dc=test" "(cn=testuser)" cn 2>/dev/null | grep -q "cn: testuser"; then
echo -e "${GREEN}[OK] Test user 'testuser' verified${NC}"
else
echo -e "${RED}[WARN] Could not verify test user 'testuser' - LDAP tests may fail${NC}"
fi
# Set environment for LDAP tests
export LDAP_URL="ldap://localhost:389"
export LDAP_BASE_DN="dc=seaweedfs,dc=test"

428
weed/iam/ldap/ldap_provider.go

@ -0,0 +1,428 @@
package ldap
import (
"context"
"crypto/tls"
"fmt"
"strings"
"sync"
"time"
"github.com/go-ldap/ldap/v3"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/iam/providers"
)
// LDAPConfig holds configuration for LDAP provider
type LDAPConfig struct {
// Server is the LDAP server URL (ldap:// or ldaps://)
Server string `json:"server"`
// BindDN is the DN used to bind for searches (optional for anonymous bind)
BindDN string `json:"bindDN,omitempty"`
// BindPassword is the password for the bind DN
BindPassword string `json:"bindPassword,omitempty"`
// BaseDN is the base DN for user searches
BaseDN string `json:"baseDN"`
// UserFilter is the filter to find users (use %s for username placeholder)
// Example: "(uid=%s)" or "(cn=%s)" or "(&(objectClass=person)(uid=%s))"
UserFilter string `json:"userFilter"`
// GroupFilter is the filter to find user groups (use %s for user DN placeholder)
// Example: "(member=%s)" or "(memberUid=%s)"
GroupFilter string `json:"groupFilter,omitempty"`
// GroupBaseDN is the base DN for group searches (defaults to BaseDN)
GroupBaseDN string `json:"groupBaseDN,omitempty"`
// Attributes to retrieve from LDAP
Attributes LDAPAttributes `json:"attributes,omitempty"`
// UseTLS enables StartTLS
UseTLS bool `json:"useTLS,omitempty"`
// InsecureSkipVerify skips TLS certificate verification
InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty"`
// ConnectionTimeout is the connection timeout
ConnectionTimeout time.Duration `json:"connectionTimeout,omitempty"`
}
// LDAPAttributes maps LDAP attribute names
type LDAPAttributes struct {
Email string `json:"email,omitempty"` // Default: mail
DisplayName string `json:"displayName,omitempty"` // Default: cn
Groups string `json:"groups,omitempty"` // Default: memberOf
UID string `json:"uid,omitempty"` // Default: uid
}
// LDAPProvider implements the IdentityProvider interface for LDAP
type LDAPProvider struct {
name string
config *LDAPConfig
initialized bool
mu sync.RWMutex
}
// NewLDAPProvider creates a new LDAP provider
func NewLDAPProvider(name string) *LDAPProvider {
return &LDAPProvider{
name: name,
}
}
// Name returns the provider name
func (p *LDAPProvider) Name() string {
return p.name
}
// Initialize initializes the provider with configuration
func (p *LDAPProvider) Initialize(config interface{}) error {
p.mu.Lock()
defer p.mu.Unlock()
cfg, ok := config.(*LDAPConfig)
if !ok {
// Try to convert from map
if cfgMap, ok := config.(map[string]interface{}); ok {
cfg = &LDAPConfig{}
if v, ok := cfgMap["server"].(string); ok {
cfg.Server = v
}
if v, ok := cfgMap["bindDN"].(string); ok {
cfg.BindDN = v
}
if v, ok := cfgMap["bindPassword"].(string); ok {
cfg.BindPassword = v
}
if v, ok := cfgMap["baseDN"].(string); ok {
cfg.BaseDN = v
}
if v, ok := cfgMap["userFilter"].(string); ok {
cfg.UserFilter = v
}
if v, ok := cfgMap["groupFilter"].(string); ok {
cfg.GroupFilter = v
}
if v, ok := cfgMap["groupBaseDN"].(string); ok {
cfg.GroupBaseDN = v
}
if v, ok := cfgMap["useTLS"].(bool); ok {
cfg.UseTLS = v
}
if v, ok := cfgMap["insecureSkipVerify"].(bool); ok {
cfg.InsecureSkipVerify = v
}
// Parse attributes
if attrs, ok := cfgMap["attributes"].(map[string]interface{}); ok {
if v, ok := attrs["email"].(string); ok {
cfg.Attributes.Email = v
}
if v, ok := attrs["displayName"].(string); ok {
cfg.Attributes.DisplayName = v
}
if v, ok := attrs["groups"].(string); ok {
cfg.Attributes.Groups = v
}
if v, ok := attrs["uid"].(string); ok {
cfg.Attributes.UID = v
}
}
} else {
return fmt.Errorf("invalid LDAP configuration type: %T", config)
}
}
// Validate required fields
if cfg.Server == "" {
return fmt.Errorf("LDAP server URL is required")
}
if cfg.BaseDN == "" {
return fmt.Errorf("LDAP base DN is required")
}
if cfg.UserFilter == "" {
cfg.UserFilter = "(cn=%s)" // Default filter
}
// Set default attributes
if cfg.Attributes.Email == "" {
cfg.Attributes.Email = "mail"
}
if cfg.Attributes.DisplayName == "" {
cfg.Attributes.DisplayName = "cn"
}
if cfg.Attributes.Groups == "" {
cfg.Attributes.Groups = "memberOf"
}
if cfg.Attributes.UID == "" {
cfg.Attributes.UID = "uid"
}
if cfg.GroupBaseDN == "" {
cfg.GroupBaseDN = cfg.BaseDN
}
if cfg.ConnectionTimeout == 0 {
cfg.ConnectionTimeout = 10 * time.Second
}
p.config = cfg
p.initialized = true
glog.V(1).Infof("LDAP provider '%s' initialized: server=%s, baseDN=%s",
p.name, cfg.Server, cfg.BaseDN)
return nil
}
// connect establishes a connection to the LDAP server
func (p *LDAPProvider) connect() (*ldap.Conn, error) {
var conn *ldap.Conn
var err error
// Parse server URL
if strings.HasPrefix(p.config.Server, "ldaps://") {
// LDAPS connection
tlsConfig := &tls.Config{
InsecureSkipVerify: p.config.InsecureSkipVerify,
}
conn, err = ldap.DialURL(p.config.Server, ldap.DialWithTLSConfig(tlsConfig))
} else {
// LDAP connection
conn, err = ldap.DialURL(p.config.Server)
if err == nil && p.config.UseTLS {
// StartTLS
tlsConfig := &tls.Config{
InsecureSkipVerify: p.config.InsecureSkipVerify,
}
err = conn.StartTLS(tlsConfig)
}
}
if err != nil {
return nil, fmt.Errorf("failed to connect to LDAP server: %w", err)
}
return conn, nil
}
// Authenticate authenticates a user with username:password credentials
func (p *LDAPProvider) Authenticate(ctx context.Context, credentials string) (*providers.ExternalIdentity, error) {
p.mu.RLock()
if !p.initialized {
p.mu.RUnlock()
return nil, fmt.Errorf("LDAP provider not initialized")
}
config := p.config
p.mu.RUnlock()
// Parse credentials (username:password format)
parts := strings.SplitN(credentials, ":", 2)
if len(parts) != 2 {
return nil, fmt.Errorf("invalid credentials format (expected username:password)")
}
username, password := parts[0], parts[1]
if username == "" || password == "" {
return nil, fmt.Errorf("username and password are required")
}
// Connect to LDAP
conn, err := p.connect()
if err != nil {
return nil, err
}
defer conn.Close()
// First, bind with service account to search for user
if config.BindDN != "" {
err = conn.Bind(config.BindDN, config.BindPassword)
if err != nil {
glog.V(2).Infof("LDAP service bind failed: %v", err)
return nil, fmt.Errorf("LDAP service bind failed: %w", err)
}
}
// Search for the user
userFilter := fmt.Sprintf(config.UserFilter, ldap.EscapeFilter(username))
searchRequest := ldap.NewSearchRequest(
config.BaseDN,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
1, // Size limit
int(config.ConnectionTimeout.Seconds()),
false,
userFilter,
[]string{"dn", config.Attributes.Email, config.Attributes.DisplayName, config.Attributes.UID, config.Attributes.Groups},
nil,
)
result, err := conn.Search(searchRequest)
if err != nil {
glog.V(2).Infof("LDAP user search failed: %v", err)
return nil, fmt.Errorf("LDAP user search failed: %w", err)
}
if len(result.Entries) == 0 {
return nil, fmt.Errorf("user not found")
}
if len(result.Entries) > 1 {
return nil, fmt.Errorf("multiple users found")
}
userEntry := result.Entries[0]
userDN := userEntry.DN
// Bind as the user to verify password
err = conn.Bind(userDN, password)
if err != nil {
glog.V(2).Infof("LDAP user bind failed for %s: %v", username, err)
return nil, fmt.Errorf("authentication failed: invalid credentials")
}
// Build identity from LDAP attributes
identity := &providers.ExternalIdentity{
UserID: username,
Email: userEntry.GetAttributeValue(config.Attributes.Email),
DisplayName: userEntry.GetAttributeValue(config.Attributes.DisplayName),
Groups: userEntry.GetAttributeValues(config.Attributes.Groups),
Provider: p.name,
Attributes: map[string]string{
"dn": userDN,
"uid": userEntry.GetAttributeValue(config.Attributes.UID),
},
}
// If no groups from memberOf, try group search
if len(identity.Groups) == 0 && config.GroupFilter != "" {
groups, err := p.searchUserGroups(conn, userDN, config)
if err != nil {
glog.V(2).Infof("Group search failed for %s: %v", username, err)
} else {
identity.Groups = groups
}
}
glog.V(2).Infof("LDAP authentication successful for user: %s, groups: %v", username, identity.Groups)
return identity, nil
}
// searchUserGroups searches for groups the user belongs to
func (p *LDAPProvider) searchUserGroups(conn *ldap.Conn, userDN string, config *LDAPConfig) ([]string, error) {
groupFilter := fmt.Sprintf(config.GroupFilter, ldap.EscapeFilter(userDN))
searchRequest := ldap.NewSearchRequest(
config.GroupBaseDN,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0,
int(config.ConnectionTimeout.Seconds()),
false,
groupFilter,
[]string{"cn", "dn"},
nil,
)
result, err := conn.Search(searchRequest)
if err != nil {
return nil, err
}
var groups []string
for _, entry := range result.Entries {
cn := entry.GetAttributeValue("cn")
if cn != "" {
groups = append(groups, cn)
}
}
return groups, nil
}
// GetUserInfo retrieves user information by user ID
func (p *LDAPProvider) GetUserInfo(ctx context.Context, userID string) (*providers.ExternalIdentity, error) {
p.mu.RLock()
if !p.initialized {
p.mu.RUnlock()
return nil, fmt.Errorf("LDAP provider not initialized")
}
config := p.config
p.mu.RUnlock()
// Connect to LDAP
conn, err := p.connect()
if err != nil {
return nil, err
}
defer conn.Close()
// Bind with service account
if config.BindDN != "" {
err = conn.Bind(config.BindDN, config.BindPassword)
if err != nil {
return nil, fmt.Errorf("LDAP service bind failed: %w", err)
}
}
// Search for the user
userFilter := fmt.Sprintf(config.UserFilter, ldap.EscapeFilter(userID))
searchRequest := ldap.NewSearchRequest(
config.BaseDN,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
1,
int(config.ConnectionTimeout.Seconds()),
false,
userFilter,
[]string{"dn", config.Attributes.Email, config.Attributes.DisplayName, config.Attributes.UID, config.Attributes.Groups},
nil,
)
result, err := conn.Search(searchRequest)
if err != nil {
return nil, fmt.Errorf("LDAP user search failed: %w", err)
}
if len(result.Entries) == 0 {
return nil, fmt.Errorf("user not found")
}
userEntry := result.Entries[0]
return &providers.ExternalIdentity{
UserID: userID,
Email: userEntry.GetAttributeValue(config.Attributes.Email),
DisplayName: userEntry.GetAttributeValue(config.Attributes.DisplayName),
Groups: userEntry.GetAttributeValues(config.Attributes.Groups),
Provider: p.name,
Attributes: map[string]string{
"dn": userEntry.DN,
"uid": userEntry.GetAttributeValue(config.Attributes.UID),
},
}, nil
}
// ValidateToken validates credentials (username:password format) and returns claims
func (p *LDAPProvider) ValidateToken(ctx context.Context, token string) (*providers.TokenClaims, error) {
identity, err := p.Authenticate(ctx, token)
if err != nil {
return nil, err
}
return &providers.TokenClaims{
Subject: identity.UserID,
Claims: map[string]interface{}{
"email": identity.Email,
"name": identity.DisplayName,
"groups": identity.Groups,
"dn": identity.Attributes["dn"],
"provider": p.name,
},
}, nil
}
// IsInitialized returns whether the provider is initialized
func (p *LDAPProvider) IsInitialized() bool {
p.mu.RLock()
defer p.mu.RUnlock()
return p.initialized
}

8
weed/iam/sts/provider_factory.go

@ -4,6 +4,7 @@ import (
"fmt"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/iam/ldap"
"github.com/seaweedfs/seaweedfs/weed/iam/oidc"
"github.com/seaweedfs/seaweedfs/weed/iam/providers"
)
@ -66,8 +67,11 @@ func (f *ProviderFactory) createOIDCProvider(config *ProviderConfig) (providers.
// createLDAPProvider creates an LDAP provider from configuration
func (f *ProviderFactory) createLDAPProvider(config *ProviderConfig) (providers.IdentityProvider, error) {
// TODO: Implement LDAP provider when available
return nil, fmt.Errorf("LDAP provider not implemented yet")
provider := ldap.NewLDAPProvider(config.Name)
if err := provider.Initialize(config.Config); err != nil {
return nil, fmt.Errorf("failed to initialize LDAP provider: %w", err)
}
return provider, nil
}
// createSAMLProvider creates a SAML provider from configuration

20
weed/s3api/s3api_server.go

@ -646,14 +646,20 @@ func (s3a *S3ApiServer) registerRouter(router *mux.Router) {
return false
}
// Parse form to check Action parameter
// This is safe because mux will call this after the request is fully read
// Check Action parameter in both form data and query string
// First try to parse form (for POST body params)
var action string
if err := r.ParseForm(); err == nil {
action := r.FormValue("Action")
// Exclude STS actions - let them be handled by STS handlers
if action == "AssumeRole" || action == "AssumeRoleWithWebIdentity" || action == "AssumeRoleWithLDAPIdentity" {
return false
}
action = r.FormValue("Action")
}
// Fallback to query string if not found in form
if action == "" {
action = r.URL.Query().Get("Action")
}
// Exclude STS actions - let them be handled by STS handlers
if action == "AssumeRole" || action == "AssumeRoleWithWebIdentity" || action == "AssumeRoleWithLDAPIdentity" {
return false
}
return true

254
weed/s3api/s3api_sts.go

@ -5,14 +5,18 @@ package s3api
// AWS SDKs to obtain temporary credentials using OIDC/JWT tokens.
import (
"crypto/rand"
"encoding/hex"
"encoding/xml"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/iam/providers"
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
)
@ -37,6 +41,65 @@ const (
stsLDAPPassword = "LDAPPassword"
)
// STS duration constants (AWS specification)
const (
minDurationSeconds = int64(900) // 15 minutes
maxDurationSeconds = int64(43200) // 12 hours
)
// parseDurationSeconds parses and validates the DurationSeconds parameter
// Returns nil if the parameter is not provided, or a pointer to the parsed value
func parseDurationSeconds(r *http.Request) (*int64, STSErrorCode, error) {
dsStr := r.FormValue("DurationSeconds")
if dsStr == "" {
return nil, "", nil
}
ds, err := strconv.ParseInt(dsStr, 10, 64)
if err != nil {
return nil, STSErrInvalidParameterValue, fmt.Errorf("invalid DurationSeconds: %w", err)
}
if ds < minDurationSeconds || ds > maxDurationSeconds {
return nil, STSErrInvalidParameterValue,
fmt.Errorf("DurationSeconds must be between %d and %d seconds", minDurationSeconds, maxDurationSeconds)
}
return &ds, "", nil
}
// generateSecureCredentials generates cryptographically secure temporary credentials
func generateSecureCredentials(userPrefix string, duration time.Duration) (accessKey, secretKey, sessionToken string, expiration time.Time, err error) {
// Generate access key with prefix and random suffix
accessKeyBytes := make([]byte, 10)
if _, err = rand.Read(accessKeyBytes); err != nil {
return "", "", "", time.Time{}, fmt.Errorf("failed to generate access key: %w", err)
}
// Use ASIA prefix for temporary credentials (AWS convention)
prefixLen := len(userPrefix)
if prefixLen > 4 {
prefixLen = 4
}
accessKey = fmt.Sprintf("ASIA%s%s", strings.ToUpper(userPrefix[:prefixLen]), hex.EncodeToString(accessKeyBytes)[:12])
// Generate cryptographically secure secret key (40 hex characters = 20 bytes)
secretKeyBytes := make([]byte, 20)
if _, err = rand.Read(secretKeyBytes); err != nil {
return "", "", "", time.Time{}, fmt.Errorf("failed to generate secret key: %w", err)
}
secretKey = hex.EncodeToString(secretKeyBytes)
// Generate session token (64 hex characters = 32 bytes)
sessionTokenBytes := make([]byte, 32)
if _, err = rand.Read(sessionTokenBytes); err != nil {
return "", "", "", time.Time{}, fmt.Errorf("failed to generate session token: %w", err)
}
sessionToken = hex.EncodeToString(sessionTokenBytes)
expiration = time.Now().Add(duration)
return accessKey, secretKey, sessionToken, expiration, nil
}
// STSHandlers provides HTTP handlers for STS operations
type STSHandlers struct {
stsService *sts.STSService
@ -110,29 +173,11 @@ func (h *STSHandlers) handleAssumeRoleWithWebIdentity(w http.ResponseWriter, r *
return
}
// Parse and validate DurationSeconds
var durationSeconds *int64
if dsStr := r.FormValue("DurationSeconds"); dsStr != "" {
ds, err := strconv.ParseInt(dsStr, 10, 64)
if err != nil {
h.writeSTSErrorResponse(w, r, STSErrInvalidParameterValue,
fmt.Errorf("invalid DurationSeconds: %w", err))
return
}
// Enforce AWS STS-compatible duration range for AssumeRoleWithWebIdentity
// AWS allows 900 seconds (15 minutes) to 43200 seconds (12 hours)
const (
minDurationSeconds = int64(900)
maxDurationSeconds = int64(43200)
)
if ds < minDurationSeconds || ds > maxDurationSeconds {
h.writeSTSErrorResponse(w, r, STSErrInvalidParameterValue,
fmt.Errorf("DurationSeconds must be between %d and %d seconds", minDurationSeconds, maxDurationSeconds))
return
}
durationSeconds = &ds
// Parse and validate DurationSeconds using helper
durationSeconds, errCode, err := parseDurationSeconds(r)
if err != nil {
h.writeSTSErrorResponse(w, r, errCode, err)
return
}
// Check if STS service is initialized
@ -211,27 +256,11 @@ func (h *STSHandlers) handleAssumeRole(w http.ResponseWriter, r *http.Request) {
return
}
// Parse and validate DurationSeconds
var durationSeconds *int64
if dsStr := r.FormValue("DurationSeconds"); dsStr != "" {
ds, err := strconv.ParseInt(dsStr, 10, 64)
if err != nil {
h.writeSTSErrorResponse(w, r, STSErrInvalidParameterValue,
fmt.Errorf("invalid DurationSeconds: %w", err))
return
}
const (
minDurationSeconds = int64(900)
maxDurationSeconds = int64(43200)
)
if ds < minDurationSeconds || ds > maxDurationSeconds {
h.writeSTSErrorResponse(w, r, STSErrInvalidParameterValue,
fmt.Errorf("DurationSeconds must be between %d and %d seconds", minDurationSeconds, maxDurationSeconds))
return
}
durationSeconds = &ds
// Parse and validate DurationSeconds using helper
durationSeconds, errCode, err := parseDurationSeconds(r)
if err != nil {
h.writeSTSErrorResponse(w, r, errCode, err)
return
}
// Check if STS service is initialized
@ -249,12 +278,11 @@ func (h *STSHandlers) handleAssumeRole(w http.ResponseWriter, r *http.Request) {
}
// Validate AWS SigV4 authentication
// shouldCheckPermissions is false because we're checking sts:AssumeRole permission ourselves
identity, _, _, _, errCode := h.iam.verifyV4Signature(r, false)
if errCode != s3err.ErrNone {
glog.V(2).Infof("AssumeRole SigV4 verification failed: %v", errCode)
identity, _, _, _, sigErrCode := h.iam.verifyV4Signature(r, false)
if sigErrCode != s3err.ErrNone {
glog.V(2).Infof("AssumeRole SigV4 verification failed: %v", sigErrCode)
h.writeSTSErrorResponse(w, r, STSErrAccessDenied,
fmt.Errorf("invalid AWS signature: %s", errCode))
fmt.Errorf("invalid AWS signature: %s", sigErrCode))
return
}
@ -267,22 +295,28 @@ func (h *STSHandlers) handleAssumeRole(w http.ResponseWriter, r *http.Request) {
glog.V(2).Infof("AssumeRole: caller identity=%s, roleArn=%s, sessionName=%s",
identity.Name, roleArn, roleSessionName)
// Calculate expiration
// Check if the caller is authorized to assume the role (sts:AssumeRole permission)
// This validates that the caller has a policy allowing sts:AssumeRole on the target role
if authErr := h.iam.VerifyActionPermission(r, identity, Action("sts:AssumeRole"), "", roleArn); authErr != s3err.ErrNone {
glog.V(2).Infof("AssumeRole: caller %s is not authorized to assume role %s", identity.Name, roleArn)
h.writeSTSErrorResponse(w, r, STSErrAccessDenied,
fmt.Errorf("user %s is not authorized to assume role %s", identity.Name, roleArn))
return
}
// Calculate duration
duration := time.Hour // Default 1 hour
if durationSeconds != nil {
duration = time.Duration(*durationSeconds) * time.Second
}
expiration := time.Now().Add(duration)
// Generate temporary credentials
// For now, create a simple credential set - in production this would use the STS service
// to generate JWT-based session tokens
nameLen := len(identity.Name)
if nameLen > 4 {
nameLen = 4
// Generate cryptographically secure temporary credentials
tempAccessKey, tempSecretKey, sessionToken, expiration, err := generateSecureCredentials(identity.Name, duration)
if err != nil {
glog.Errorf("AssumeRole: failed to generate credentials: %v", err)
h.writeSTSErrorResponse(w, r, STSErrInternalError, err)
return
}
tempAccessKey := fmt.Sprintf("ASIA%s%d", identity.Name[:nameLen], time.Now().UnixNano()%1000000)
tempSecretKey := fmt.Sprintf("%x", time.Now().UnixNano())
// Build and return response
xmlResponse := &AssumeRoleResponse{
@ -290,7 +324,7 @@ func (h *STSHandlers) handleAssumeRole(w http.ResponseWriter, r *http.Request) {
Credentials: STSCredentials{
AccessKeyId: tempAccessKey,
SecretAccessKey: tempSecretKey,
SessionToken: fmt.Sprintf("FwoGZXIvY...%s", identity.Name), // Placeholder token
SessionToken: sessionToken,
Expiration: expiration.Format(time.RFC3339),
},
AssumedRoleUser: &AssumedRoleUser{
@ -337,27 +371,11 @@ func (h *STSHandlers) handleAssumeRoleWithLDAPIdentity(w http.ResponseWriter, r
return
}
// Parse and validate DurationSeconds
var durationSeconds *int64
if dsStr := r.FormValue("DurationSeconds"); dsStr != "" {
ds, err := strconv.ParseInt(dsStr, 10, 64)
if err != nil {
h.writeSTSErrorResponse(w, r, STSErrInvalidParameterValue,
fmt.Errorf("invalid DurationSeconds: %w", err))
return
}
const (
minDurationSeconds = int64(900)
maxDurationSeconds = int64(43200)
)
if ds < minDurationSeconds || ds > maxDurationSeconds {
h.writeSTSErrorResponse(w, r, STSErrInvalidParameterValue,
fmt.Errorf("DurationSeconds must be between %d and %d seconds", minDurationSeconds, maxDurationSeconds))
return
}
durationSeconds = &ds
// Parse and validate DurationSeconds using helper
durationSeconds, errCode, err := parseDurationSeconds(r)
if err != nil {
h.writeSTSErrorResponse(w, r, errCode, err)
return
}
// Check if STS service is initialized
@ -367,19 +385,69 @@ func (h *STSHandlers) handleAssumeRoleWithLDAPIdentity(w http.ResponseWriter, r
return
}
// TODO: Implement LDAP authentication
// For now, return an error indicating this feature is not yet fully implemented
glog.V(2).Infof("AssumeRoleWithLDAPIdentity request: user=%s, role=%s", ldapUsername, roleArn)
h.writeSTSErrorResponse(w, r, STSErrAccessDenied,
fmt.Errorf("AssumeRoleWithLDAPIdentity requires LDAP provider configuration - feature implementation in progress"))
// Find an LDAP provider from the registered providers
var ldapProvider providers.IdentityProvider
for _, provider := range h.stsService.GetProviders() {
// Check if this is an LDAP provider by looking at the name or type
if strings.Contains(strings.ToLower(provider.Name()), "ldap") {
ldapProvider = provider
break
}
}
if ldapProvider == nil {
glog.V(2).Infof("AssumeRoleWithLDAPIdentity: no LDAP provider configured")
h.writeSTSErrorResponse(w, r, STSErrAccessDenied,
fmt.Errorf("no LDAP provider configured - please add an LDAP provider to IAM configuration"))
return
}
// Authenticate with LDAP provider
// The provider expects credentials in "username:password" format
credentials := ldapUsername + ":" + ldapPassword
identity, err := ldapProvider.Authenticate(r.Context(), credentials)
if err != nil {
glog.V(2).Infof("AssumeRoleWithLDAPIdentity: LDAP authentication failed for user %s: %v", ldapUsername, err)
h.writeSTSErrorResponse(w, r, STSErrAccessDenied,
fmt.Errorf("LDAP authentication failed: %v", err))
return
}
glog.V(2).Infof("AssumeRoleWithLDAPIdentity: user %s authenticated successfully, groups=%v",
ldapUsername, identity.Groups)
// Calculate duration
duration := time.Hour // Default 1 hour
if durationSeconds != nil {
duration = time.Duration(*durationSeconds) * time.Second
}
// Generate cryptographically secure temporary credentials
tempAccessKey, tempSecretKey, sessionToken, expiration, err := generateSecureCredentials(ldapUsername, duration)
if err != nil {
glog.Errorf("AssumeRoleWithLDAPIdentity: failed to generate credentials: %v", err)
h.writeSTSErrorResponse(w, r, STSErrInternalError, err)
return
}
// Build and return response
xmlResponse := &AssumeRoleWithLDAPIdentityResponse{
Result: LDAPIdentityResult{
Credentials: STSCredentials{
AccessKeyId: tempAccessKey,
SecretAccessKey: tempSecretKey,
SessionToken: sessionToken,
Expiration: expiration.Format(time.RFC3339),
},
AssumedRoleUser: &AssumedRoleUser{
AssumedRoleId: fmt.Sprintf("%s:%s", roleArn, roleSessionName),
Arn: fmt.Sprintf("arn:aws:sts::assumed-role/%s/%s", roleArn, roleSessionName),
},
},
}
xmlResponse.ResponseMetadata.RequestId = fmt.Sprintf("%d", time.Now().UnixNano())
// Once LDAP provider is implemented, the flow would be:
// 1. Authenticate user against configured LDAP server
// 2. Extract user groups and attributes from LDAP
// 3. Verify the user has permission to assume the specified role
// 4. Generate temporary credentials using sts.STSService.AssumeRoleWithCredentials
// 5. Return AssumeRoleWithLDAPIdentityResponse with credentials
_ = durationSeconds // Will be used when implementation is complete
s3err.WriteXMLResponse(w, r, http.StatusOK, xmlResponse)
}
// STS Response types for XML marshaling

Loading…
Cancel
Save