Browse Source

Refactor s3api: Extract generic ExecuteAction method for IAM operations

pull/8126/head
Chris Lu 4 weeks ago
parent
commit
3c9a7efde9
  1. 125
      weed/s3api/s3api_embedded_iam.go
  2. 42
      weed/s3api/s3api_embedded_iam_test.go

125
weed/s3api/s3api_embedded_iam.go

@ -34,6 +34,10 @@ type EmbeddedIamApi struct {
credentialManager *credential.CredentialManager credentialManager *credential.CredentialManager
iam *IdentityAccessManagement iam *IdentityAccessManagement
policyLock sync.RWMutex policyLock sync.RWMutex
// Test hook
getS3ApiConfigurationFunc func(*iam_pb.S3ApiConfiguration) error
putS3ApiConfigurationFunc func(*iam_pb.S3ApiConfiguration) error
reloadConfigurationFunc func() error
} }
// NewEmbeddedIamApi creates a new embedded IAM API handler. // NewEmbeddedIamApi creates a new embedded IAM API handler.
@ -165,6 +169,9 @@ func (e *EmbeddedIamApi) writeIamErrorResponse(w http.ResponseWriter, r *http.Re
// GetS3ApiConfiguration loads the S3 API configuration from the credential manager. // GetS3ApiConfiguration loads the S3 API configuration from the credential manager.
func (e *EmbeddedIamApi) GetS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) error { func (e *EmbeddedIamApi) GetS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) error {
if e.getS3ApiConfigurationFunc != nil {
return e.getS3ApiConfigurationFunc(s3cfg)
}
config, err := e.credentialManager.LoadConfiguration(context.Background()) config, err := e.credentialManager.LoadConfiguration(context.Background())
if err != nil { if err != nil {
return fmt.Errorf("failed to load configuration: %w", err) return fmt.Errorf("failed to load configuration: %w", err)
@ -175,9 +182,20 @@ func (e *EmbeddedIamApi) GetS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration)
// PutS3ApiConfiguration saves the S3 API configuration to the credential manager. // PutS3ApiConfiguration saves the S3 API configuration to the credential manager.
func (e *EmbeddedIamApi) PutS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) error { func (e *EmbeddedIamApi) PutS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) error {
if e.putS3ApiConfigurationFunc != nil {
return e.putS3ApiConfigurationFunc(s3cfg)
}
return e.credentialManager.SaveConfiguration(context.Background(), s3cfg) return e.credentialManager.SaveConfiguration(context.Background(), s3cfg)
} }
// ReloadConfiguration reloads the IAM configuration from the credential manager.
func (e *EmbeddedIamApi) ReloadConfiguration() error {
if e.reloadConfigurationFunc != nil {
return e.reloadConfigurationFunc()
}
return e.iam.LoadS3ApiConfigurationFromCredentialManager()
}
// ListUsers lists all IAM users. // ListUsers lists all IAM users.
func (e *EmbeddedIamApi) ListUsers(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) iamListUsersResponse { func (e *EmbeddedIamApi) ListUsers(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) iamListUsersResponse {
var resp iamListUsersResponse var resp iamListUsersResponse
@ -1024,79 +1042,66 @@ func (e *EmbeddedIamApi) AuthIam(f http.HandlerFunc, _ Action) http.HandlerFunc
} }
} }
// DoActions handles IAM API actions.
func (e *EmbeddedIamApi) DoActions(w http.ResponseWriter, r *http.Request) {
// ExecuteAction executes an IAM action with the given values.
func (e *EmbeddedIamApi) ExecuteAction(values url.Values) (interface{}, *iamError) {
// Lock to prevent concurrent read-modify-write race conditions // Lock to prevent concurrent read-modify-write race conditions
e.policyLock.Lock() e.policyLock.Lock()
defer e.policyLock.Unlock() defer e.policyLock.Unlock()
if err := r.ParseForm(); err != nil {
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
return
}
values := r.PostForm
s3cfg := &iam_pb.S3ApiConfiguration{} s3cfg := &iam_pb.S3ApiConfiguration{}
if err := e.GetS3ApiConfiguration(s3cfg); err != nil && !errors.Is(err, filer_pb.ErrNotFound) { if err := e.GetS3ApiConfiguration(s3cfg); err != nil && !errors.Is(err, filer_pb.ErrNotFound) {
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError)
return
return nil, &iamError{Code: s3err.GetAPIError(s3err.ErrInternalError).Code, Error: fmt.Errorf("failed to get s3 api configuration: %v", err)}
} }
glog.V(4).Infof("IAM DoActions: %+v", values)
glog.V(4).Infof("IAM ExecuteAction: %+v", values)
var response interface{} var response interface{}
var iamErr *iamError var iamErr *iamError
changed := true changed := true
switch r.Form.Get("Action") {
switch values.Get("Action") {
case "ListUsers": case "ListUsers":
response = e.ListUsers(s3cfg, values) response = e.ListUsers(s3cfg, values)
changed = false changed = false
case "ListAccessKeys": case "ListAccessKeys":
e.handleImplicitUsername(r, values)
// Note: handleImplicitUsername requires request context which we don't have here for gRPC
// gRPC callers must provide UserName explicitly
response = e.ListAccessKeys(s3cfg, values) response = e.ListAccessKeys(s3cfg, values)
changed = false changed = false
case "CreateUser": case "CreateUser":
response, iamErr = e.CreateUser(s3cfg, values) response, iamErr = e.CreateUser(s3cfg, values)
if iamErr != nil { if iamErr != nil {
e.writeIamErrorResponse(w, r, iamErr)
return
return nil, iamErr
} }
case "GetUser": case "GetUser":
userName := values.Get("UserName") userName := values.Get("UserName")
response, iamErr = e.GetUser(s3cfg, userName) response, iamErr = e.GetUser(s3cfg, userName)
if iamErr != nil { if iamErr != nil {
e.writeIamErrorResponse(w, r, iamErr)
return
return nil, iamErr
} }
changed = false changed = false
case "UpdateUser": case "UpdateUser":
response, iamErr = e.UpdateUser(s3cfg, values) response, iamErr = e.UpdateUser(s3cfg, values)
if iamErr != nil { if iamErr != nil {
e.writeIamErrorResponse(w, r, iamErr)
return
return nil, iamErr
} }
case "DeleteUser": case "DeleteUser":
userName := values.Get("UserName") userName := values.Get("UserName")
response, iamErr = e.DeleteUser(s3cfg, userName) response, iamErr = e.DeleteUser(s3cfg, userName)
if iamErr != nil { if iamErr != nil {
e.writeIamErrorResponse(w, r, iamErr)
return
return nil, iamErr
} }
case "CreateAccessKey": case "CreateAccessKey":
e.handleImplicitUsername(r, values)
response, iamErr = e.CreateAccessKey(s3cfg, values) response, iamErr = e.CreateAccessKey(s3cfg, values)
if iamErr != nil { if iamErr != nil {
glog.Errorf("CreateAccessKey: %+v", iamErr.Error) glog.Errorf("CreateAccessKey: %+v", iamErr.Error)
e.writeIamErrorResponse(w, r, iamErr)
return
return nil, iamErr
} }
case "DeleteAccessKey": case "DeleteAccessKey":
e.handleImplicitUsername(r, values)
response = e.DeleteAccessKey(s3cfg, values) response = e.DeleteAccessKey(s3cfg, values)
case "CreatePolicy": case "CreatePolicy":
response, iamErr = e.CreatePolicy(s3cfg, values) response, iamErr = e.CreatePolicy(s3cfg, values)
if iamErr != nil { if iamErr != nil {
glog.Errorf("CreatePolicy: %+v", iamErr.Error) glog.Errorf("CreatePolicy: %+v", iamErr.Error)
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
return
return nil, &iamError{Code: s3err.GetAPIError(s3err.ErrInvalidRequest).Code, Error: iamErr.Error}
} }
case "DeletePolicy": case "DeletePolicy":
// Managed policies are not stored separately, so deletion is a no-op. // Managed policies are not stored separately, so deletion is a no-op.
@ -1107,48 +1112,40 @@ func (e *EmbeddedIamApi) DoActions(w http.ResponseWriter, r *http.Request) {
response, iamErr = e.PutUserPolicy(s3cfg, values) response, iamErr = e.PutUserPolicy(s3cfg, values)
if iamErr != nil { if iamErr != nil {
glog.Errorf("PutUserPolicy: %+v", iamErr.Error) glog.Errorf("PutUserPolicy: %+v", iamErr.Error)
e.writeIamErrorResponse(w, r, iamErr)
return
return nil, iamErr
} }
case "GetUserPolicy": case "GetUserPolicy":
response, iamErr = e.GetUserPolicy(s3cfg, values) response, iamErr = e.GetUserPolicy(s3cfg, values)
if iamErr != nil { if iamErr != nil {
e.writeIamErrorResponse(w, r, iamErr)
return
return nil, iamErr
} }
changed = false changed = false
case "DeleteUserPolicy": case "DeleteUserPolicy":
response, iamErr = e.DeleteUserPolicy(s3cfg, values) response, iamErr = e.DeleteUserPolicy(s3cfg, values)
if iamErr != nil { if iamErr != nil {
e.writeIamErrorResponse(w, r, iamErr)
return
return nil, iamErr
} }
case "SetUserStatus": case "SetUserStatus":
response, iamErr = e.SetUserStatus(s3cfg, values) response, iamErr = e.SetUserStatus(s3cfg, values)
if iamErr != nil { if iamErr != nil {
e.writeIamErrorResponse(w, r, iamErr)
return
return nil, iamErr
} }
case "UpdateAccessKey": case "UpdateAccessKey":
e.handleImplicitUsername(r, values)
response, iamErr = e.UpdateAccessKey(s3cfg, values) response, iamErr = e.UpdateAccessKey(s3cfg, values)
if iamErr != nil { if iamErr != nil {
e.writeIamErrorResponse(w, r, iamErr)
return
return nil, iamErr
} }
// Service Account actions // Service Account actions
case "CreateServiceAccount": case "CreateServiceAccount":
createdBy := s3_constants.GetIdentityNameFromContext(r)
createdBy := values.Get("CreatedBy")
response, iamErr = e.CreateServiceAccount(s3cfg, values, createdBy) response, iamErr = e.CreateServiceAccount(s3cfg, values, createdBy)
if iamErr != nil { if iamErr != nil {
e.writeIamErrorResponse(w, r, iamErr)
return
return nil, iamErr
} }
case "DeleteServiceAccount": case "DeleteServiceAccount":
response, iamErr = e.DeleteServiceAccount(s3cfg, values) response, iamErr = e.DeleteServiceAccount(s3cfg, values)
if iamErr != nil { if iamErr != nil {
e.writeIamErrorResponse(w, r, iamErr)
return
return nil, iamErr
} }
case "ListServiceAccounts": case "ListServiceAccounts":
response = e.ListServiceAccounts(s3cfg, values) response = e.ListServiceAccounts(s3cfg, values)
@ -1156,37 +1153,55 @@ func (e *EmbeddedIamApi) DoActions(w http.ResponseWriter, r *http.Request) {
case "GetServiceAccount": case "GetServiceAccount":
response, iamErr = e.GetServiceAccount(s3cfg, values) response, iamErr = e.GetServiceAccount(s3cfg, values)
if iamErr != nil { if iamErr != nil {
e.writeIamErrorResponse(w, r, iamErr)
return
return nil, iamErr
} }
changed = false changed = false
case "UpdateServiceAccount": case "UpdateServiceAccount":
response, iamErr = e.UpdateServiceAccount(s3cfg, values) response, iamErr = e.UpdateServiceAccount(s3cfg, values)
if iamErr != nil { if iamErr != nil {
e.writeIamErrorResponse(w, r, iamErr)
return
return nil, iamErr
} }
default: default:
errNotImplemented := s3err.GetAPIError(s3err.ErrNotImplemented)
errorResponse := iamErrorResponse{}
errorResponse.Error.Code = &errNotImplemented.Code
errorResponse.Error.Message = &errNotImplemented.Description
s3err.WriteXMLResponse(w, r, errNotImplemented.HTTPStatusCode, errorResponse)
return
return nil, &iamError{Code: s3err.GetAPIError(s3err.ErrNotImplemented).Code, Error: errors.New(s3err.GetAPIError(s3err.ErrNotImplemented).Description)}
} }
if changed { if changed {
if err := e.PutS3ApiConfiguration(s3cfg); err != nil { if err := e.PutS3ApiConfiguration(s3cfg); err != nil {
iamErr = &iamError{Code: iam.ErrCodeServiceFailureException, Error: err} iamErr = &iamError{Code: iam.ErrCodeServiceFailureException, Error: err}
e.writeIamErrorResponse(w, r, iamErr)
return
return nil, iamErr
} }
// Reload in-memory identity maps so subsequent LookupByAccessKey calls // Reload in-memory identity maps so subsequent LookupByAccessKey calls
// can see newly created or deleted keys immediately // can see newly created or deleted keys immediately
if err := e.iam.LoadS3ApiConfigurationFromCredentialManager(); err != nil {
if err := e.ReloadConfiguration(); err != nil {
glog.Warningf("Failed to reload IAM configuration after mutation: %v", err) glog.Warningf("Failed to reload IAM configuration after mutation: %v", err)
// Don't fail the request since the persistent save succeeded // Don't fail the request since the persistent save succeeded
} }
} }
return response, nil
}
// DoActions handles IAM API actions.
func (e *EmbeddedIamApi) DoActions(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
return
}
values := r.PostForm
// Handle implicit username for HTTP requests
switch r.Form.Get("Action") {
case "ListAccessKeys", "CreateAccessKey", "DeleteAccessKey", "UpdateAccessKey":
e.handleImplicitUsername(r, values)
case "CreateServiceAccount":
createdBy := s3_constants.GetIdentityNameFromContext(r)
values.Set("CreatedBy", createdBy)
}
response, iamErr := e.ExecuteAction(values)
if iamErr != nil {
e.writeIamErrorResponse(w, r, iamErr)
return
}
// Set RequestId for AWS compatibility // Set RequestId for AWS compatibility
if r, ok := response.(interface{ SetRequestId() }); ok { if r, ok := response.(interface{ SetRequestId() }); ok {
r.SetRequestId() r.SetRequestId()

42
weed/s3api/s3api_embedded_iam_test.go

@ -36,6 +36,20 @@ func NewEmbeddedIamApiForTest() *EmbeddedIamApiForTest {
}, },
mockConfig: &iam_pb.S3ApiConfiguration{}, mockConfig: &iam_pb.S3ApiConfiguration{},
} }
e.getS3ApiConfigurationFunc = func(s3cfg *iam_pb.S3ApiConfiguration) error {
if e.mockConfig != nil {
cloned := proto.Clone(e.mockConfig).(*iam_pb.S3ApiConfiguration)
proto.Merge(s3cfg, cloned)
}
return nil
}
e.putS3ApiConfigurationFunc = func(s3cfg *iam_pb.S3ApiConfiguration) error {
e.mockConfig = proto.Clone(s3cfg).(*iam_pb.S3ApiConfiguration)
return nil
}
e.reloadConfigurationFunc = func() error {
return nil
}
return e return e
} }
@ -1661,3 +1675,31 @@ func TestOldCodeOrderWouldFail(t *testing.T) {
t.Log("This demonstrates the bug: ParseForm before auth causes SignatureDoesNotMatch") t.Log("This demonstrates the bug: ParseForm before auth causes SignatureDoesNotMatch")
} }
// TestEmbeddedIamExecuteAction tests calling ExecuteAction directly
func TestEmbeddedIamExecuteAction(t *testing.T) {
api := NewEmbeddedIamApiForTest()
api.mockConfig = &iam_pb.S3ApiConfiguration{}
// Explicitly set hook to debug panic
api.EmbeddedIamApi.reloadConfigurationFunc = func() error {
return nil
}
// Test case: CreateUser via ExecuteAction
vals := url.Values{}
vals.Set("Action", "CreateUser")
vals.Set("UserName", "ExecuteActionUser")
resp, iamErr := api.ExecuteAction(vals)
assert.Nil(t, iamErr)
// Verify response type
createResp, ok := resp.(iamCreateUserResponse)
assert.True(t, ok)
assert.Equal(t, "ExecuteActionUser", *createResp.CreateUserResult.User.UserName)
// Verify persistence
assert.Len(t, api.mockConfig.Identities, 1)
assert.Equal(t, "ExecuteActionUser", api.mockConfig.Identities[0].Name)
}
Loading…
Cancel
Save