aboutsummaryrefslogtreecommitdiff
path: root/weed/iamapi/iamapi_management_handlers.go
diff options
context:
space:
mode:
Diffstat (limited to 'weed/iamapi/iamapi_management_handlers.go')
-rw-r--r--weed/iamapi/iamapi_management_handlers.go157
1 files changed, 121 insertions, 36 deletions
diff --git a/weed/iamapi/iamapi_management_handlers.go b/weed/iamapi/iamapi_management_handlers.go
index 1a8f852cd..1985b042f 100644
--- a/weed/iamapi/iamapi_management_handlers.go
+++ b/weed/iamapi/iamapi_management_handlers.go
@@ -1,17 +1,22 @@
package iamapi
+// This file provides IAM API handlers for the standalone IAM server.
+// NOTE: There is code duplication with weed/s3api/s3api_embedded_iam.go.
+// See GitHub issue #7747 for the planned refactoring to extract common IAM logic
+// into a shared package.
+
import (
+ "crypto/rand"
"crypto/sha1"
"encoding/json"
"errors"
"fmt"
- "math/rand"
+ "math/big"
"net/http"
"net/url"
- "reflect"
+ "sort"
"strings"
"sync"
- "time"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
@@ -38,8 +43,6 @@ const (
)
var (
- seededRand *rand.Rand = rand.New(
- rand.NewSource(time.Now().UnixNano()))
policyDocuments = map[string]*policy_engine.PolicyDocument{}
policyLock = sync.RWMutex{}
)
@@ -104,12 +107,45 @@ func Hash(s *string) string {
return fmt.Sprintf("%x", h.Sum(nil))
}
-func StringWithCharset(length int, charset string) string {
+// StringWithCharset generates a cryptographically secure random string.
+// Uses crypto/rand for security-sensitive credential generation.
+func StringWithCharset(length int, charset string) (string, error) {
+ if length <= 0 {
+ return "", fmt.Errorf("length must be positive, got %d", length)
+ }
+ if charset == "" {
+ return "", fmt.Errorf("charset must not be empty")
+ }
b := make([]byte, length)
for i := range b {
- b[i] = charset[seededRand.Intn(len(charset))]
+ n, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
+ if err != nil {
+ return "", fmt.Errorf("failed to generate random index: %w", err)
+ }
+ b[i] = charset[n.Int64()]
}
- return string(b)
+ return string(b), nil
+}
+
+// stringSlicesEqual compares two string slices for equality, ignoring order.
+// This is used instead of reflect.DeepEqual to avoid order-dependent comparisons.
+func stringSlicesEqual(a, b []string) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ // Make copies to avoid modifying the originals
+ aCopy := make([]string, len(a))
+ bCopy := make([]string, len(b))
+ copy(aCopy, a)
+ copy(bCopy, b)
+ sort.Strings(aCopy)
+ sort.Strings(bCopy)
+ for i := range aCopy {
+ if aCopy[i] != bCopy[i] {
+ return false
+ }
+ }
+ return true
}
func (iama *IamApiServer) ListUsers(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp ListUsersResponse) {
@@ -199,8 +235,7 @@ func (iama *IamApiServer) CreatePolicy(s3cfg *iam_pb.S3ApiConfiguration, values
resp.CreatePolicyResult.Policy.Arn = &arn
resp.CreatePolicyResult.Policy.PolicyId = &policyId
policies := Policies{}
- policyLock.Lock()
- defer policyLock.Unlock()
+ // Note: Lock is already held by DoActions, no need to acquire here
if err = iama.s3ApiConfig.GetPolicies(&policies); err != nil {
return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: err}
}
@@ -273,7 +308,8 @@ func (iama *IamApiServer) GetUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values
for resource, actions := range statements {
isEqAction := false
for i, statement := range policyDocument.Statement {
- if reflect.DeepEqual(statement.Action.Strings(), actions) {
+ // Use order-independent comparison to avoid duplicates from different action orderings
+ if stringSlicesEqual(statement.Action.Strings(), actions) {
policyDocument.Statement[i].Resource = policy_engine.NewStringOrStringSlice(append(
policyDocument.Statement[i].Resource.Strings(), resource)...)
isEqAction = true
@@ -300,11 +336,12 @@ func (iama *IamApiServer) GetUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values
return resp, &IamError{Code: iam.ErrCodeNoSuchEntityException, Error: fmt.Errorf(USER_DOES_NOT_EXIST, userName)}
}
-func (iama *IamApiServer) DeleteUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp PutUserPolicyResponse, err *IamError) {
+// DeleteUserPolicy removes the inline policy from a user (clears their actions).
+func (iama *IamApiServer) DeleteUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp DeleteUserPolicyResponse, err *IamError) {
userName := values.Get("UserName")
- for i, ident := range s3cfg.Identities {
+ for _, ident := range s3cfg.Identities {
if ident.Name == userName {
- s3cfg.Identities = append(s3cfg.Identities[:i], s3cfg.Identities[i+1:]...)
+ ident.Actions = nil
return resp, nil
}
}
@@ -348,11 +385,19 @@ func GetActions(policy *policy_engine.PolicyDocument) ([]string, error) {
return actions, nil
}
-func (iama *IamApiServer) CreateAccessKey(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp CreateAccessKeyResponse) {
+func (iama *IamApiServer) CreateAccessKey(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp CreateAccessKeyResponse, iamErr *IamError) {
userName := values.Get("UserName")
status := iam.StatusTypeActive
- accessKeyId := StringWithCharset(21, charsetUpper)
- secretAccessKey := StringWithCharset(42, charset)
+
+ accessKeyId, err := StringWithCharset(21, charsetUpper)
+ if err != nil {
+ return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("failed to generate access key: %w", err)}
+ }
+ secretAccessKey, err := StringWithCharset(42, charset)
+ if err != nil {
+ return resp, &IamError{Code: iam.ErrCodeServiceFailureException, Error: fmt.Errorf("failed to generate secret key: %w", err)}
+ }
+
resp.CreateAccessKeyResult.AccessKey.AccessKeyId = &accessKeyId
resp.CreateAccessKeyResult.AccessKey.SecretAccessKey = &secretAccessKey
resp.CreateAccessKeyResult.AccessKey.UserName = &userName
@@ -379,7 +424,7 @@ func (iama *IamApiServer) CreateAccessKey(s3cfg *iam_pb.S3ApiConfiguration, valu
},
)
}
- return resp
+ return resp, nil
}
func (iama *IamApiServer) DeleteAccessKey(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp DeleteAccessKeyResponse) {
@@ -399,36 +444,60 @@ func (iama *IamApiServer) DeleteAccessKey(s3cfg *iam_pb.S3ApiConfiguration, valu
return resp
}
-// handleImplicitUsername adds username who signs the request to values if 'username' is not specified
-// According to https://awscli.amazonaws.com/v2/documentation/api/latest/reference/iam/create-access-key.html/
-// "If you do not specify a user name, IAM determines the user name implicitly based on the Amazon Web
-// Services access key ID signing the request."
-func handleImplicitUsername(r *http.Request, values url.Values) {
+// handleImplicitUsername adds username who signs the request to values if 'username' is not specified.
+// According to AWS documentation: "If you do not specify a user name, IAM determines the user name
+// implicitly based on the Amazon Web Services access key ID signing the request."
+// This function extracts the AccessKeyId from the SigV4 credential and looks up the corresponding
+// identity name in the credential store.
+func (iama *IamApiServer) handleImplicitUsername(r *http.Request, values url.Values) {
if len(r.Header["Authorization"]) == 0 || values.Get("UserName") != "" {
return
}
- // get username who signs the request. For a typical Authorization:
- // "AWS4-HMAC-SHA256 Credential=197FSAQ7HHTA48X64O3A/20220420/test1/iam/aws4_request, SignedHeaders=content-type;
- // host;x-amz-date, Signature=6757dc6b3d7534d67e17842760310e99ee695408497f6edc4fdb84770c252dc8",
- // the "test1" will be extracted as the username
- glog.V(4).Infof("Authorization field: %v", r.Header["Authorization"][0])
+ // Log presence of auth header without exposing sensitive signature material
+ glog.V(4).Infof("Authorization header present, extracting access key")
+ // Parse AWS SigV4 Authorization header format:
+ // "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/iam/aws4_request, ..."
s := strings.Split(r.Header["Authorization"][0], "Credential=")
if len(s) < 2 {
return
}
s = strings.Split(s[1], ",")
- if len(s) < 2 {
+ if len(s) < 1 {
return
}
s = strings.Split(s[0], "/")
- if len(s) < 5 {
+ if len(s) < 1 {
+ return
+ }
+ // s[0] is the AccessKeyId
+ accessKeyId := s[0]
+ if accessKeyId == "" {
+ return
+ }
+ // Nil-guard: ensure iam is initialized before lookup
+ if iama.iam == nil {
+ glog.V(4).Infof("IAM not initialized, cannot look up access key")
+ return
+ }
+ // Look up the identity by access key to get the username
+ identity, _, found := iama.iam.LookupByAccessKey(accessKeyId)
+ if !found {
+ // Mask access key in logs - show only first 4 chars
+ maskedKey := accessKeyId
+ if len(accessKeyId) > 4 {
+ maskedKey = accessKeyId[:4] + "***"
+ }
+ glog.V(4).Infof("Access key %s not found in credential store", maskedKey)
return
}
- userName := s[2]
- values.Set("UserName", userName)
+ values.Set("UserName", identity.Name)
}
func (iama *IamApiServer) DoActions(w http.ResponseWriter, r *http.Request) {
+ // Lock to prevent concurrent read-modify-write race conditions
+ policyLock.Lock()
+ defer policyLock.Unlock()
+
if err := r.ParseForm(); err != nil {
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
return
@@ -449,7 +518,7 @@ func (iama *IamApiServer) DoActions(w http.ResponseWriter, r *http.Request) {
response = iama.ListUsers(s3cfg, values)
changed = false
case "ListAccessKeys":
- handleImplicitUsername(r, values)
+ iama.handleImplicitUsername(r, values)
response = iama.ListAccessKeys(s3cfg, values)
changed = false
case "CreateUser":
@@ -477,10 +546,15 @@ func (iama *IamApiServer) DoActions(w http.ResponseWriter, r *http.Request) {
return
}
case "CreateAccessKey":
- handleImplicitUsername(r, values)
- response = iama.CreateAccessKey(s3cfg, values)
+ iama.handleImplicitUsername(r, values)
+ response, iamError = iama.CreateAccessKey(s3cfg, values)
+ if iamError != nil {
+ glog.Errorf("CreateAccessKey: %+v", iamError.Error)
+ writeIamErrorResponse(w, r, iamError)
+ return
+ }
case "DeleteAccessKey":
- handleImplicitUsername(r, values)
+ iama.handleImplicitUsername(r, values)
response = iama.DeleteAccessKey(s3cfg, values)
case "CreatePolicy":
response, iamError = iama.CreatePolicy(s3cfg, values)
@@ -489,6 +563,9 @@ func (iama *IamApiServer) DoActions(w http.ResponseWriter, r *http.Request) {
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
return
}
+ // CreatePolicy persists the policy document via iama.s3ApiConfig.PutPolicies().
+ // The `changed` flag is false because this does not modify the main s3cfg.Identities configuration.
+ changed = false
case "PutUserPolicy":
var iamError *IamError
response, iamError = iama.PutUserPolicy(s3cfg, values)
@@ -525,6 +602,14 @@ func (iama *IamApiServer) DoActions(w http.ResponseWriter, r *http.Request) {
writeIamErrorResponse(w, r, &iamError)
return
}
+ // Reload in-memory identity maps so subsequent LookupByAccessKey calls
+ // can see newly created or deleted keys immediately
+ if iama.iam != nil {
+ if err := iama.iam.LoadS3ApiConfigurationFromCredentialManager(); err != nil {
+ glog.Warningf("Failed to reload IAM configuration after mutation: %v", err)
+ // Don't fail the request since the persistent save succeeded
+ }
+ }
}
s3err.WriteXMLResponse(w, r, http.StatusOK, response)
}