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:
Chris Lu
2026-01-26 13:38:15 -08:00
committed by GitHub
parent c5b53397c6
commit 43229b05ce
11 changed files with 1844 additions and 669 deletions

View File

@@ -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()