Explicit IAM gRPC APIs for S3 Server (#8126)
* Update IAM and S3 protobuf definitions for explicit IAM gRPC APIs * Refactor s3api: Extract generic ExecuteAction method for IAM operations * Implement explicit IAM gRPC APIs in S3 server * iam: remove deprecated GetConfiguration and PutConfiguration RPCs * iamapi: refactor handlers to use CredentialManager directly * s3api: refactor embedded IAM to use CredentialManager directly * server: remove deprecated configuration gRPC handlers * credential/grpc: refactor configuration calls to return error * shell: update s3.configure to list users instead of full config * s3api: fix CreateServiceAccount gRPC handler to map required fields * s3api: fix UpdateServiceAccount gRPC handler to map fields and safe status * s3api: enforce UserName in embedded IAM ListAccessKeys * test: fix test_config.json structure to match proto definition * Revert "credential/grpc: refactor configuration calls to return error" This reverts commit cde707dd8b88c7d1bd730271518542eceb5ed069. * Revert "server: remove deprecated configuration gRPC handlers" This reverts commit 7307e205a083c8315cf84ddc2614b3e50eda2e33. * Revert "s3api: enforce UserName in embedded IAM ListAccessKeys" This reverts commit adf727ba52b4f3ffb911f0d0df85db858412ff83. * Revert "s3api: fix UpdateServiceAccount gRPC handler to map fields and safe status" This reverts commit 6a4be3314d43b6c8fda8d5e0558e83e87a19df3f. * Revert "s3api: fix CreateServiceAccount gRPC handler to map required fields" This reverts commit 9bb4425f07fbad38fb68d33e5c0aa573d8912a37. * Revert "shell: update s3.configure to list users instead of full config" This reverts commit f3304ead537b3e6be03d46df4cb55983ab931726. * Revert "s3api: refactor embedded IAM to use CredentialManager directly" This reverts commit 9012f27af82d11f0e824877712a5ae2505a65f86. * Revert "iamapi: refactor handlers to use CredentialManager directly" This reverts commit 3a148212236576b0a3aa4d991c2abb014fb46091. * Revert "iam: remove deprecated GetConfiguration and PutConfiguration RPCs" This reverts commit e16e08aa0099699338d3155bc7428e1051ce0a6a. * s3api: address IAM code review comments (error handling, logging, gRPC response mapping) * s3api: add robustness to startup by retrying KEK and IAM config loading from Filer * s3api: address IAM gRPC code review comments (safety, validation, status logic) * fix return
This commit is contained in:
@@ -34,6 +34,10 @@ type EmbeddedIamApi struct {
|
||||
credentialManager *credential.CredentialManager
|
||||
iam *IdentityAccessManagement
|
||||
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.
|
||||
@@ -165,6 +169,9 @@ func (e *EmbeddedIamApi) writeIamErrorResponse(w http.ResponseWriter, r *http.Re
|
||||
|
||||
// GetS3ApiConfiguration loads the S3 API configuration from the credential manager.
|
||||
func (e *EmbeddedIamApi) GetS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) error {
|
||||
if e.getS3ApiConfigurationFunc != nil {
|
||||
return e.getS3ApiConfigurationFunc(s3cfg)
|
||||
}
|
||||
config, err := e.credentialManager.LoadConfiguration(context.Background())
|
||||
if err != nil {
|
||||
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.
|
||||
func (e *EmbeddedIamApi) PutS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) error {
|
||||
if e.putS3ApiConfigurationFunc != nil {
|
||||
return e.putS3ApiConfigurationFunc(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.
|
||||
func (e *EmbeddedIamApi) ListUsers(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) 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
|
||||
e.policyLock.Lock()
|
||||
defer e.policyLock.Unlock()
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
|
||||
return
|
||||
}
|
||||
values := r.PostForm
|
||||
s3cfg := &iam_pb.S3ApiConfiguration{}
|
||||
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 iamErr *iamError
|
||||
changed := true
|
||||
switch r.Form.Get("Action") {
|
||||
switch values.Get("Action") {
|
||||
case "ListUsers":
|
||||
response = e.ListUsers(s3cfg, values)
|
||||
changed = false
|
||||
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)
|
||||
changed = false
|
||||
case "CreateUser":
|
||||
response, iamErr = e.CreateUser(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
return nil, iamErr
|
||||
}
|
||||
case "GetUser":
|
||||
userName := values.Get("UserName")
|
||||
response, iamErr = e.GetUser(s3cfg, userName)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
return nil, iamErr
|
||||
}
|
||||
changed = false
|
||||
case "UpdateUser":
|
||||
response, iamErr = e.UpdateUser(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
return nil, iamErr
|
||||
}
|
||||
case "DeleteUser":
|
||||
userName := values.Get("UserName")
|
||||
response, iamErr = e.DeleteUser(s3cfg, userName)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
return nil, iamErr
|
||||
}
|
||||
case "CreateAccessKey":
|
||||
e.handleImplicitUsername(r, values)
|
||||
response, iamErr = e.CreateAccessKey(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
glog.Errorf("CreateAccessKey: %+v", iamErr.Error)
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
return nil, iamErr
|
||||
}
|
||||
case "DeleteAccessKey":
|
||||
e.handleImplicitUsername(r, values)
|
||||
response = e.DeleteAccessKey(s3cfg, values)
|
||||
case "CreatePolicy":
|
||||
response, iamErr = e.CreatePolicy(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
glog.Errorf("CreatePolicy: %+v", iamErr.Error)
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest)
|
||||
return
|
||||
return nil, iamErr
|
||||
}
|
||||
case "DeletePolicy":
|
||||
// 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)
|
||||
if iamErr != nil {
|
||||
glog.Errorf("PutUserPolicy: %+v", iamErr.Error)
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
return nil, iamErr
|
||||
}
|
||||
case "GetUserPolicy":
|
||||
response, iamErr = e.GetUserPolicy(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
return nil, iamErr
|
||||
}
|
||||
changed = false
|
||||
case "DeleteUserPolicy":
|
||||
response, iamErr = e.DeleteUserPolicy(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
return nil, iamErr
|
||||
}
|
||||
case "SetUserStatus":
|
||||
response, iamErr = e.SetUserStatus(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
return nil, iamErr
|
||||
}
|
||||
case "UpdateAccessKey":
|
||||
e.handleImplicitUsername(r, values)
|
||||
response, iamErr = e.UpdateAccessKey(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
return nil, iamErr
|
||||
}
|
||||
// Service Account actions
|
||||
case "CreateServiceAccount":
|
||||
createdBy := s3_constants.GetIdentityNameFromContext(r)
|
||||
createdBy := values.Get("CreatedBy")
|
||||
response, iamErr = e.CreateServiceAccount(s3cfg, values, createdBy)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
return nil, iamErr
|
||||
}
|
||||
case "DeleteServiceAccount":
|
||||
response, iamErr = e.DeleteServiceAccount(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
return nil, iamErr
|
||||
}
|
||||
case "ListServiceAccounts":
|
||||
response = e.ListServiceAccounts(s3cfg, values)
|
||||
@@ -1156,37 +1153,55 @@ func (e *EmbeddedIamApi) DoActions(w http.ResponseWriter, r *http.Request) {
|
||||
case "GetServiceAccount":
|
||||
response, iamErr = e.GetServiceAccount(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
return nil, iamErr
|
||||
}
|
||||
changed = false
|
||||
case "UpdateServiceAccount":
|
||||
response, iamErr = e.UpdateServiceAccount(s3cfg, values)
|
||||
if iamErr != nil {
|
||||
e.writeIamErrorResponse(w, r, iamErr)
|
||||
return
|
||||
return nil, iamErr
|
||||
}
|
||||
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 err := e.PutS3ApiConfiguration(s3cfg); err != nil {
|
||||
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
|
||||
// can see newly created or deleted keys immediately
|
||||
if err := e.iam.LoadS3ApiConfigurationFromCredentialManager(); err != nil {
|
||||
glog.Warningf("Failed to reload IAM configuration after mutation: %v", err)
|
||||
if err := e.ReloadConfiguration(); err != nil {
|
||||
glog.Errorf("Failed to reload IAM configuration after mutation: %v", err)
|
||||
// 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
|
||||
if r, ok := response.(interface{ SetRequestId() }); ok {
|
||||
r.SetRequestId()
|
||||
|
||||
Reference in New Issue
Block a user