* 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
345 lines
11 KiB
Go
345 lines
11 KiB
Go
package s3api
|
|
|
|
import (
|
|
"context"
|
|
|
|
"fmt"
|
|
"net/url"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
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
|
|
}
|