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:
@@ -3,6 +3,7 @@ package s3api
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -346,7 +347,23 @@ func (iam *IdentityAccessManagement) loadEnvironmentVariableCredentials() {
|
||||
}
|
||||
}
|
||||
|
||||
func (iam *IdentityAccessManagement) loadS3ApiConfigurationFromFiler(option *S3ApiServerOption) error {
|
||||
func (iam *IdentityAccessManagement) loadS3ApiConfigurationFromFiler(option *S3ApiServerOption) (err error) {
|
||||
// Try to load configuration with retries to handle transient connectivity issues during startup
|
||||
for i := 0; i < 10; i++ {
|
||||
err = iam.doLoadS3ApiConfigurationFromFiler(option)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, filer_pb.ErrNotFound) {
|
||||
return err
|
||||
}
|
||||
glog.Warningf("fail to load config from filer (attempt %d/10): %v", i+1, err)
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (iam *IdentityAccessManagement) doLoadS3ApiConfigurationFromFiler(option *S3ApiServerOption) error {
|
||||
return iam.LoadS3ApiConfigurationFromCredentialManager()
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
@@ -278,26 +279,27 @@ func (km *SSES3KeyManager) InitializeWithFiler(filerClient filer_pb.FilerClient)
|
||||
|
||||
km.filerClient = filerClient
|
||||
|
||||
// Try to load existing KEK from filer
|
||||
if err := km.loadSuperKeyFromFiler(); err != nil {
|
||||
// Only generate a new key if it does not exist.
|
||||
// For other errors (e.g. connectivity), we should fail fast to prevent creating a new key
|
||||
// and making existing data undecryptable.
|
||||
// Try to load existing KEK from filer with retries to handle transient connectivity issues during startup
|
||||
var err error
|
||||
for i := 0; i < 10; i++ {
|
||||
err = km.loadSuperKeyFromFiler()
|
||||
if err == nil {
|
||||
glog.V(1).Infof("SSE-S3 KeyManager: Loaded KEK from filer %s", km.kekPath)
|
||||
return nil
|
||||
}
|
||||
if errors.Is(err, filer_pb.ErrNotFound) {
|
||||
glog.V(1).Infof("SSE-S3 KeyManager: KEK not found, generating new KEK (load from filer %s: %v)", km.kekPath, err)
|
||||
if genErr := km.generateAndSaveSuperKeyToFiler(); genErr != nil {
|
||||
return fmt.Errorf("failed to generate and save SSE-S3 super key: %w", genErr)
|
||||
}
|
||||
} else {
|
||||
// A different error occurred (e.g., network issue, permission denied).
|
||||
// Return the error to prevent starting with a broken state.
|
||||
return fmt.Errorf("failed to load SSE-S3 super key from %s: %w", km.kekPath, err)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
glog.V(1).Infof("SSE-S3 KeyManager: Loaded KEK from filer %s", km.kekPath)
|
||||
glog.Warningf("SSE-S3 KeyManager: failed to load KEK (attempt %d/10): %v", i+1, err)
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
return nil
|
||||
// If we're here, all retries failed
|
||||
return fmt.Errorf("failed to load SSE-S3 super key from %s after 10 attempts: %w", km.kekPath, err)
|
||||
}
|
||||
|
||||
// loadSuperKeyFromFiler loads the KEK from the filer
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -36,6 +36,20 @@ func NewEmbeddedIamApiForTest() *EmbeddedIamApiForTest {
|
||||
},
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1661,3 +1675,31 @@ func TestOldCodeOrderWouldFail(t *testing.T) {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -3,15 +3,342 @@ package s3api
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/s3_pb"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
||||
)
|
||||
|
||||
func (s3a *S3ApiServer) Configure(ctx context.Context, request *s3_pb.S3ConfigureRequest) (*s3_pb.S3ConfigureResponse, error) {
|
||||
func (s3a *S3ApiServer) executeAction(values url.Values) (interface{}, error) {
|
||||
if s3a.embeddedIam == nil {
|
||||
return nil, fmt.Errorf("embedded iam is disabled")
|
||||
}
|
||||
response, iamErr := s3a.embeddedIam.ExecuteAction(values)
|
||||
if iamErr != nil {
|
||||
return nil, fmt.Errorf("IAM error: %s - %v", iamErr.Code, iamErr.Error)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
if err := s3a.iam.LoadS3ApiConfigurationFromBytes(request.S3ConfigurationFileContent); err != nil {
|
||||
func (s3a *S3ApiServer) ListUsers(ctx context.Context, req *iam_pb.ListUsersRequest) (*iam_pb.ListUsersResponse, error) {
|
||||
values := url.Values{}
|
||||
values.Set("Action", "ListUsers")
|
||||
resp, err := s3a.executeAction(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &s3_pb.S3ConfigureResponse{}, nil
|
||||
|
||||
iamResp, ok := resp.(iamListUsersResponse)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected IAM ListUsers response type %T", resp)
|
||||
}
|
||||
var usernames []string
|
||||
for _, user := range iamResp.ListUsersResult.Users {
|
||||
if user != nil && user.UserName != nil {
|
||||
usernames = append(usernames, *user.UserName)
|
||||
}
|
||||
}
|
||||
return &iam_pb.ListUsersResponse{Usernames: usernames}, nil
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) CreateUser(ctx context.Context, req *iam_pb.CreateUserRequest) (*iam_pb.CreateUserResponse, error) {
|
||||
if req.Identity == nil || req.Identity.Name == "" {
|
||||
return nil, fmt.Errorf("username name is required")
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("Action", "CreateUser")
|
||||
values.Set("UserName", req.Identity.Name)
|
||||
_, err := s3a.executeAction(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &iam_pb.CreateUserResponse{}, nil
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) GetUser(ctx context.Context, req *iam_pb.GetUserRequest) (*iam_pb.GetUserResponse, error) {
|
||||
if req.Username == "" {
|
||||
return nil, fmt.Errorf("username is required")
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("Action", "GetUser")
|
||||
values.Set("UserName", req.Username)
|
||||
resp, err := s3a.executeAction(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iamResp, ok := resp.(iamGetUserResponse)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected IAM GetUser response type %T", resp)
|
||||
}
|
||||
|
||||
var username string
|
||||
if iamResp.GetUserResult.User.UserName != nil {
|
||||
username = *iamResp.GetUserResult.User.UserName
|
||||
}
|
||||
|
||||
return &iam_pb.GetUserResponse{
|
||||
Identity: &iam_pb.Identity{
|
||||
Name: username,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) UpdateUser(ctx context.Context, req *iam_pb.UpdateUserRequest) (*iam_pb.UpdateUserResponse, error) {
|
||||
if req.Username == "" {
|
||||
return nil, fmt.Errorf("username is required")
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("Action", "UpdateUser")
|
||||
values.Set("UserName", req.Username)
|
||||
// UpdateUser in DoActions expects "NewUserName" if renaming, but CreateUser just takes UserName.
|
||||
// Looking at s3api_embedded_iam.go, UpdateUser uses "NewUserName" to change name.
|
||||
if req.Identity != nil && req.Identity.Name != "" {
|
||||
values.Set("NewUserName", req.Identity.Name)
|
||||
}
|
||||
_, err := s3a.executeAction(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &iam_pb.UpdateUserResponse{}, nil
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) DeleteUser(ctx context.Context, req *iam_pb.DeleteUserRequest) (*iam_pb.DeleteUserResponse, error) {
|
||||
if req.Username == "" {
|
||||
return nil, fmt.Errorf("username is required")
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("Action", "DeleteUser")
|
||||
values.Set("UserName", req.Username)
|
||||
_, err := s3a.executeAction(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &iam_pb.DeleteUserResponse{}, nil
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) ListAccessKeys(ctx context.Context, req *iam_pb.ListAccessKeysRequest) (*iam_pb.ListAccessKeysResponse, error) {
|
||||
if req.Username == "" {
|
||||
return nil, fmt.Errorf("username is required")
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("Action", "ListAccessKeys")
|
||||
values.Set("UserName", req.Username)
|
||||
resp, err := s3a.executeAction(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iamResp, ok := resp.(iamListAccessKeysResponse)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected IAM ListAccessKeys response type %T", resp)
|
||||
}
|
||||
var accessKeys []*iam_pb.Credential
|
||||
for _, meta := range iamResp.ListAccessKeysResult.AccessKeyMetadata {
|
||||
if meta != nil && meta.AccessKeyId != nil && meta.Status != nil {
|
||||
accessKeys = append(accessKeys, &iam_pb.Credential{
|
||||
AccessKey: *meta.AccessKeyId,
|
||||
Status: *meta.Status,
|
||||
})
|
||||
}
|
||||
}
|
||||
return &iam_pb.ListAccessKeysResponse{AccessKeys: accessKeys}, nil
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) CreateAccessKey(ctx context.Context, req *iam_pb.CreateAccessKeyRequest) (*iam_pb.CreateAccessKeyResponse, error) {
|
||||
if req.Username == "" {
|
||||
return nil, fmt.Errorf("username is required")
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("Action", "CreateAccessKey")
|
||||
values.Set("UserName", req.Username)
|
||||
_, err := s3a.executeAction(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &iam_pb.CreateAccessKeyResponse{}, nil
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) DeleteAccessKey(ctx context.Context, req *iam_pb.DeleteAccessKeyRequest) (*iam_pb.DeleteAccessKeyResponse, error) {
|
||||
if req.Username == "" {
|
||||
return nil, fmt.Errorf("username is required")
|
||||
}
|
||||
if req.AccessKey == "" {
|
||||
return nil, fmt.Errorf("access key is required")
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("Action", "DeleteAccessKey")
|
||||
values.Set("UserName", req.Username)
|
||||
values.Set("AccessKeyId", req.AccessKey)
|
||||
_, err := s3a.executeAction(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &iam_pb.DeleteAccessKeyResponse{}, nil
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) PutUserPolicy(ctx context.Context, req *iam_pb.PutUserPolicyRequest) (*iam_pb.PutUserPolicyResponse, error) {
|
||||
if req.Username == "" {
|
||||
return nil, fmt.Errorf("username is required")
|
||||
}
|
||||
if req.PolicyName == "" {
|
||||
return nil, fmt.Errorf("policy name is required")
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("Action", "PutUserPolicy")
|
||||
values.Set("UserName", req.Username)
|
||||
values.Set("PolicyName", req.PolicyName)
|
||||
values.Set("PolicyDocument", req.PolicyDocument)
|
||||
_, err := s3a.executeAction(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &iam_pb.PutUserPolicyResponse{}, nil
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) GetUserPolicy(ctx context.Context, req *iam_pb.GetUserPolicyRequest) (*iam_pb.GetUserPolicyResponse, error) {
|
||||
if req.Username == "" {
|
||||
return nil, fmt.Errorf("username is required")
|
||||
}
|
||||
if req.PolicyName == "" {
|
||||
return nil, fmt.Errorf("policy name is required")
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("Action", "GetUserPolicy")
|
||||
values.Set("UserName", req.Username)
|
||||
values.Set("PolicyName", req.PolicyName)
|
||||
resp, err := s3a.executeAction(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iamResp, ok := resp.(iamGetUserPolicyResponse)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected IAM GetUserPolicy response type %T", resp)
|
||||
}
|
||||
return &iam_pb.GetUserPolicyResponse{
|
||||
Username: iamResp.GetUserPolicyResult.UserName,
|
||||
PolicyName: iamResp.GetUserPolicyResult.PolicyName,
|
||||
PolicyDocument: iamResp.GetUserPolicyResult.PolicyDocument,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) DeleteUserPolicy(ctx context.Context, req *iam_pb.DeleteUserPolicyRequest) (*iam_pb.DeleteUserPolicyResponse, error) {
|
||||
if req.Username == "" {
|
||||
return nil, fmt.Errorf("username is required")
|
||||
}
|
||||
if req.PolicyName == "" {
|
||||
return nil, fmt.Errorf("policy name is required")
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("Action", "DeleteUserPolicy")
|
||||
values.Set("UserName", req.Username)
|
||||
values.Set("PolicyName", req.PolicyName)
|
||||
_, err := s3a.executeAction(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &iam_pb.DeleteUserPolicyResponse{}, nil
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) ListServiceAccounts(ctx context.Context, req *iam_pb.ListServiceAccountsRequest) (*iam_pb.ListServiceAccountsResponse, error) {
|
||||
values := url.Values{}
|
||||
values.Set("Action", "ListServiceAccounts")
|
||||
resp, err := s3a.executeAction(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iamResp, ok := resp.(iamListServiceAccountsResponse)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected IAM ListServiceAccounts response type %T", resp)
|
||||
}
|
||||
var serviceAccounts []*iam_pb.ServiceAccount
|
||||
for _, sa := range iamResp.ListServiceAccountsResult.ServiceAccounts {
|
||||
if sa != nil {
|
||||
serviceAccounts = append(serviceAccounts, &iam_pb.ServiceAccount{
|
||||
Id: sa.ServiceAccountId,
|
||||
ParentUser: sa.ParentUser,
|
||||
Description: sa.Description,
|
||||
Credential: &iam_pb.Credential{
|
||||
AccessKey: sa.AccessKeyId,
|
||||
Status: sa.Status,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
return &iam_pb.ListServiceAccountsResponse{ServiceAccounts: serviceAccounts}, nil
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) CreateServiceAccount(ctx context.Context, req *iam_pb.CreateServiceAccountRequest) (*iam_pb.CreateServiceAccountResponse, error) {
|
||||
if req.ServiceAccount == nil || req.ServiceAccount.CreatedBy == "" {
|
||||
return nil, fmt.Errorf("service account owner is required")
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("Action", "CreateServiceAccount")
|
||||
values.Set("CreatedBy", req.ServiceAccount.CreatedBy)
|
||||
_, err := s3a.executeAction(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &iam_pb.CreateServiceAccountResponse{}, nil
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) UpdateServiceAccount(ctx context.Context, req *iam_pb.UpdateServiceAccountRequest) (*iam_pb.UpdateServiceAccountResponse, error) {
|
||||
if req.Id == "" {
|
||||
return nil, fmt.Errorf("service account id is required")
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("Action", "UpdateServiceAccount")
|
||||
values.Set("ServiceAccountId", req.Id)
|
||||
if req.ServiceAccount != nil && req.ServiceAccount.Disabled {
|
||||
values.Set("Status", "Inactive")
|
||||
}
|
||||
_, err := s3a.executeAction(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &iam_pb.UpdateServiceAccountResponse{}, nil
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) DeleteServiceAccount(ctx context.Context, req *iam_pb.DeleteServiceAccountRequest) (*iam_pb.DeleteServiceAccountResponse, error) {
|
||||
if req.Id == "" {
|
||||
return nil, fmt.Errorf("service account id is required")
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("Action", "DeleteServiceAccount")
|
||||
values.Set("ServiceAccountId", req.Id)
|
||||
_, err := s3a.executeAction(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &iam_pb.DeleteServiceAccountResponse{}, nil
|
||||
}
|
||||
|
||||
func (s3a *S3ApiServer) GetServiceAccount(ctx context.Context, req *iam_pb.GetServiceAccountRequest) (*iam_pb.GetServiceAccountResponse, error) {
|
||||
if req.Id == "" {
|
||||
return nil, fmt.Errorf("service account id is required")
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("Action", "GetServiceAccount")
|
||||
values.Set("ServiceAccountId", req.Id)
|
||||
resp, err := s3a.executeAction(values)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
iamResp, ok := resp.(iamGetServiceAccountResponse)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unexpected IAM GetServiceAccount response type %T", resp)
|
||||
}
|
||||
|
||||
var serviceAccount *iam_pb.ServiceAccount
|
||||
sa := iamResp.GetServiceAccountResult.ServiceAccount
|
||||
serviceAccount = &iam_pb.ServiceAccount{
|
||||
Id: sa.ServiceAccountId,
|
||||
ParentUser: sa.ParentUser,
|
||||
Description: sa.Description,
|
||||
Credential: &iam_pb.Credential{
|
||||
AccessKey: sa.AccessKeyId,
|
||||
Status: sa.Status,
|
||||
},
|
||||
}
|
||||
|
||||
return &iam_pb.GetServiceAccountResponse{
|
||||
ServiceAccount: serviceAccount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user