committed by
							
								 GitHub
								GitHub
							
						
					
				
				
				  
				  No known key found for this signature in database
				  
				  	
						GPG Key ID: 4AEE18F83AFDEB23
				  	
				  
				
			
		
		
		
	
				 9 changed files with 901 additions and 6 deletions
			
			
		- 
					1weed/command/command.go
- 
					25weed/command/filer.go
- 
					97weed/command/iam.go
- 
					8weed/filer/read_write.go
- 
					100weed/iamapi/iamapi_handlers.go
- 
					322weed/iamapi/iamapi_management_handlers.go
- 
					93weed/iamapi/iamapi_response.go
- 
					104weed/iamapi/iamapi_server.go
- 
					157weed/iamapi/iamapi_test.go
| @ -0,0 +1,97 @@ | |||
| package command | |||
| 
 | |||
| import ( | |||
| 	"context" | |||
| 	"fmt" | |||
| 	"net/http" | |||
| 
 | |||
| 	"github.com/chrislusf/seaweedfs/weed/glog" | |||
| 	"github.com/chrislusf/seaweedfs/weed/iamapi" | |||
| 	"github.com/chrislusf/seaweedfs/weed/pb" | |||
| 	"github.com/chrislusf/seaweedfs/weed/pb/filer_pb" | |||
| 	"github.com/chrislusf/seaweedfs/weed/security" | |||
| 	"github.com/chrislusf/seaweedfs/weed/util" | |||
| 	"github.com/gorilla/mux" | |||
| 	"time" | |||
| ) | |||
| 
 | |||
| var ( | |||
| 	iamStandaloneOptions IamOptions | |||
| ) | |||
| 
 | |||
| type IamOptions struct { | |||
| 	filer   *string | |||
| 	masters *string | |||
| 	port    *int | |||
| } | |||
| 
 | |||
| func init() { | |||
| 	cmdIam.Run = runIam // break init cycle
 | |||
| 	iamStandaloneOptions.filer = cmdIam.Flag.String("filer", "localhost:8888", "filer server address") | |||
| 	iamStandaloneOptions.masters = cmdIam.Flag.String("master", "localhost:9333", "comma-separated master servers") | |||
| 	iamStandaloneOptions.port = cmdIam.Flag.Int("port", 8111, "iam server http listen port") | |||
| } | |||
| 
 | |||
| var cmdIam = &Command{ | |||
| 	UsageLine: "iam [-port=8111] [-filer=<ip:port>] [-masters=<ip:port>,<ip:port>]", | |||
| 	Short:     "start a iam API compatible server", | |||
| 	Long:      "start a iam API compatible server.", | |||
| } | |||
| 
 | |||
| func runIam(cmd *Command, args []string) bool { | |||
| 	return iamStandaloneOptions.startIamServer() | |||
| } | |||
| 
 | |||
| func (iamopt *IamOptions) startIamServer() bool { | |||
| 	filerGrpcAddress, err := pb.ParseServerToGrpcAddress(*iamopt.filer) | |||
| 	if err != nil { | |||
| 		glog.Fatal(err) | |||
| 		return false | |||
| 	} | |||
| 
 | |||
| 	grpcDialOption := security.LoadClientTLS(util.GetViper(), "grpc.client") | |||
| 	for { | |||
| 		err = pb.WithGrpcFilerClient(filerGrpcAddress, grpcDialOption, func(client filer_pb.SeaweedFilerClient) error { | |||
| 			resp, err := client.GetFilerConfiguration(context.Background(), &filer_pb.GetFilerConfigurationRequest{}) | |||
| 			if err != nil { | |||
| 				return fmt.Errorf("get filer %s configuration: %v", filerGrpcAddress, err) | |||
| 			} | |||
| 			glog.V(0).Infof("IAM read filer configuration: %s", resp) | |||
| 			return nil | |||
| 		}) | |||
| 		if err != nil { | |||
| 			glog.V(0).Infof("wait to connect to filer %s grpc address %s", *iamopt.filer, filerGrpcAddress) | |||
| 			time.Sleep(time.Second) | |||
| 		} else { | |||
| 			glog.V(0).Infof("connected to filer %s grpc address %s", *iamopt.filer, filerGrpcAddress) | |||
| 			break | |||
| 		} | |||
| 	} | |||
| 
 | |||
| 	router := mux.NewRouter().SkipClean(true) | |||
| 	_, iamApiServer_err := iamapi.NewIamApiServer(router, &iamapi.IamServerOption{ | |||
| 		Filer:            *iamopt.filer, | |||
| 		Port:             *iamopt.port, | |||
| 		FilerGrpcAddress: filerGrpcAddress, | |||
| 		GrpcDialOption:   grpcDialOption, | |||
| 	}) | |||
| 	glog.V(0).Info("NewIamApiServer created") | |||
| 	if iamApiServer_err != nil { | |||
| 		glog.Fatalf("IAM API Server startup error: %v", iamApiServer_err) | |||
| 	} | |||
| 
 | |||
| 	httpS := &http.Server{Handler: router} | |||
| 
 | |||
| 	listenAddress := fmt.Sprintf(":%d", *iamopt.port) | |||
| 	iamApiListener, err := util.NewListener(listenAddress, time.Duration(10)*time.Second) | |||
| 	if err != nil { | |||
| 		glog.Fatalf("IAM API Server listener on %s error: %v", listenAddress, err) | |||
| 	} | |||
| 
 | |||
| 	glog.V(0).Infof("Start Seaweed IAM API Server %s at http port %d", util.Version(), *iamopt.port) | |||
| 	if err = httpS.Serve(iamApiListener); err != nil { | |||
| 		glog.Fatalf("IAM API Server Fail to serve: %v", err) | |||
| 	} | |||
| 
 | |||
| 	return true | |||
| } | |||
| @ -0,0 +1,100 @@ | |||
| package iamapi | |||
| 
 | |||
| import ( | |||
| 	"bytes" | |||
| 	"encoding/xml" | |||
| 	"fmt" | |||
| 	"strconv" | |||
| 
 | |||
| 	"net/http" | |||
| 	"net/url" | |||
| 	"time" | |||
| 
 | |||
| 	"github.com/chrislusf/seaweedfs/weed/glog" | |||
| 	"github.com/chrislusf/seaweedfs/weed/s3api/s3err" | |||
| 
 | |||
| 	"github.com/aws/aws-sdk-go/service/iam" | |||
| ) | |||
| 
 | |||
| type mimeType string | |||
| 
 | |||
| const ( | |||
| 	mimeNone mimeType = "" | |||
| 	mimeXML  mimeType = "application/xml" | |||
| ) | |||
| 
 | |||
| func setCommonHeaders(w http.ResponseWriter) { | |||
| 	w.Header().Set("x-amz-request-id", fmt.Sprintf("%d", time.Now().UnixNano())) | |||
| 	w.Header().Set("Accept-Ranges", "bytes") | |||
| } | |||
| 
 | |||
| // Encodes the response headers into XML format.
 | |||
| func encodeResponse(response interface{}) []byte { | |||
| 	var bytesBuffer bytes.Buffer | |||
| 	bytesBuffer.WriteString(xml.Header) | |||
| 	e := xml.NewEncoder(&bytesBuffer) | |||
| 	e.Encode(response) | |||
| 	return bytesBuffer.Bytes() | |||
| } | |||
| 
 | |||
| // If none of the http routes match respond with MethodNotAllowed
 | |||
| func notFoundHandler(w http.ResponseWriter, r *http.Request) { | |||
| 	glog.V(0).Infof("unsupported %s %s", r.Method, r.RequestURI) | |||
| 	writeErrorResponse(w, s3err.ErrMethodNotAllowed, r.URL) | |||
| } | |||
| 
 | |||
| func writeErrorResponse(w http.ResponseWriter, errorCode s3err.ErrorCode, reqURL *url.URL) { | |||
| 	apiError := s3err.GetAPIError(errorCode) | |||
| 	errorResponse := getRESTErrorResponse(apiError, reqURL.Path) | |||
| 	encodedErrorResponse := encodeResponse(errorResponse) | |||
| 	writeResponse(w, apiError.HTTPStatusCode, encodedErrorResponse, mimeXML) | |||
| } | |||
| 
 | |||
| func writeIamErrorResponse(w http.ResponseWriter, err error, object string, value string) { | |||
| 	errCode := err.Error() | |||
| 	errorResp := ErrorResponse{} | |||
| 	errorResp.Error.Type = "Sender" | |||
| 	errorResp.Error.Code = &errCode | |||
| 	glog.Errorf("Response %+v", err) | |||
| 	switch errCode { | |||
| 	case iam.ErrCodeNoSuchEntityException: | |||
| 		msg := fmt.Sprintf("The %s with name %s cannot be found.", object, value) | |||
| 		errorResp.Error.Message = &msg | |||
| 		writeResponse(w, http.StatusNotFound, encodeResponse(errorResp), mimeXML) | |||
| 	default: | |||
| 		writeResponse(w, http.StatusInternalServerError, encodeResponse(errorResp), mimeXML) | |||
| 
 | |||
| 	} | |||
| } | |||
| 
 | |||
| func getRESTErrorResponse(err s3err.APIError, resource string) s3err.RESTErrorResponse { | |||
| 	return s3err.RESTErrorResponse{ | |||
| 		Code:      err.Code, | |||
| 		Message:   err.Description, | |||
| 		Resource:  resource, | |||
| 		RequestID: fmt.Sprintf("%d", time.Now().UnixNano()), | |||
| 	} | |||
| } | |||
| 
 | |||
| func writeResponse(w http.ResponseWriter, statusCode int, response []byte, mType mimeType) { | |||
| 	setCommonHeaders(w) | |||
| 	if response != nil { | |||
| 		w.Header().Set("Content-Length", strconv.Itoa(len(response))) | |||
| 	} | |||
| 	if mType != mimeNone { | |||
| 		w.Header().Set("Content-Type", string(mType)) | |||
| 	} | |||
| 	w.WriteHeader(statusCode) | |||
| 	if response != nil { | |||
| 		glog.V(4).Infof("status %d %s: %s", statusCode, mType, string(response)) | |||
| 		_, err := w.Write(response) | |||
| 		if err != nil { | |||
| 			glog.V(0).Infof("write err: %v", err) | |||
| 		} | |||
| 		w.(http.Flusher).Flush() | |||
| 	} | |||
| } | |||
| 
 | |||
| func writeSuccessResponseXML(w http.ResponseWriter, response []byte) { | |||
| 	writeResponse(w, http.StatusOK, response, mimeXML) | |||
| } | |||
| @ -0,0 +1,322 @@ | |||
| package iamapi | |||
| 
 | |||
| import ( | |||
| 	"crypto/sha1" | |||
| 	"encoding/json" | |||
| 	"fmt" | |||
| 	"github.com/chrislusf/seaweedfs/weed/glog" | |||
| 	"github.com/chrislusf/seaweedfs/weed/pb/iam_pb" | |||
| 	"github.com/chrislusf/seaweedfs/weed/s3api/s3_constants" | |||
| 	"github.com/chrislusf/seaweedfs/weed/s3api/s3err" | |||
| 	"math/rand" | |||
| 	"net/http" | |||
| 	"net/url" | |||
| 	"strings" | |||
| 	"sync" | |||
| 	"time" | |||
| 
 | |||
| 	"github.com/aws/aws-sdk-go/service/iam" | |||
| ) | |||
| 
 | |||
| const ( | |||
| 	charsetUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" | |||
| 	charset      = charsetUpper + "abcdefghijklmnopqrstuvwxyz/" | |||
| ) | |||
| 
 | |||
| var ( | |||
| 	seededRand *rand.Rand = rand.New( | |||
| 		rand.NewSource(time.Now().UnixNano())) | |||
| 	policyDocuments = map[string]*PolicyDocument{} | |||
| ) | |||
| 
 | |||
| type Statement struct { | |||
| 	Effect   string   `json:"Effect"` | |||
| 	Action   []string `json:"Action"` | |||
| 	Resource []string `json:"Resource"` | |||
| } | |||
| 
 | |||
| type PolicyDocument struct { | |||
| 	Version   string       `json:"Version"` | |||
| 	Statement []*Statement `json:"Statement"` | |||
| } | |||
| 
 | |||
| func Hash(s *string) string { | |||
| 	h := sha1.New() | |||
| 	h.Write([]byte(*s)) | |||
| 	return fmt.Sprintf("%x", h.Sum(nil)) | |||
| } | |||
| 
 | |||
| func StringWithCharset(length int, charset string) string { | |||
| 	b := make([]byte, length) | |||
| 	for i := range b { | |||
| 		b[i] = charset[seededRand.Intn(len(charset))] | |||
| 	} | |||
| 	return string(b) | |||
| } | |||
| 
 | |||
| func (iama *IamApiServer) ListUsers(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp ListUsersResponse) { | |||
| 	for _, ident := range s3cfg.Identities { | |||
| 		resp.ListUsersResult.Users = append(resp.ListUsersResult.Users, &iam.User{UserName: &ident.Name}) | |||
| 	} | |||
| 	return resp | |||
| } | |||
| 
 | |||
| func (iama *IamApiServer) ListAccessKeys(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp ListAccessKeysResponse) { | |||
| 	status := iam.StatusTypeActive | |||
| 	for _, ident := range s3cfg.Identities { | |||
| 		for _, cred := range ident.Credentials { | |||
| 			resp.ListAccessKeysResult.AccessKeyMetadata = append(resp.ListAccessKeysResult.AccessKeyMetadata, | |||
| 				&iam.AccessKeyMetadata{UserName: &ident.Name, AccessKeyId: &cred.AccessKey, Status: &status}, | |||
| 			) | |||
| 		} | |||
| 	} | |||
| 	return resp | |||
| } | |||
| 
 | |||
| func (iama *IamApiServer) CreateUser(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp CreateUserResponse) { | |||
| 	userName := values.Get("UserName") | |||
| 	resp.CreateUserResult.User.UserName = &userName | |||
| 	s3cfg.Identities = append(s3cfg.Identities, &iam_pb.Identity{Name: userName}) | |||
| 	return resp | |||
| } | |||
| 
 | |||
| func (iama *IamApiServer) DeleteUser(s3cfg *iam_pb.S3ApiConfiguration, userName string) (resp DeleteUserResponse, err error) { | |||
| 	for i, ident := range s3cfg.Identities { | |||
| 		if userName == ident.Name { | |||
| 			ident.Credentials = append(ident.Credentials[:i], ident.Credentials[i+1:]...) | |||
| 			return resp, nil | |||
| 		} | |||
| 	} | |||
| 	return resp, fmt.Errorf(iam.ErrCodeNoSuchEntityException) | |||
| } | |||
| 
 | |||
| func (iama *IamApiServer) GetUser(s3cfg *iam_pb.S3ApiConfiguration, userName string) (resp GetUserResponse, err error) { | |||
| 	for _, ident := range s3cfg.Identities { | |||
| 		if userName == ident.Name { | |||
| 			resp.GetUserResult.User = iam.User{UserName: &ident.Name} | |||
| 			return resp, nil | |||
| 		} | |||
| 	} | |||
| 	return resp, fmt.Errorf(iam.ErrCodeNoSuchEntityException) | |||
| } | |||
| 
 | |||
| func GetPolicyDocument(policy *string) (policyDocument PolicyDocument, err error) { | |||
| 	if err = json.Unmarshal([]byte(*policy), &policyDocument); err != nil { | |||
| 		return PolicyDocument{}, err | |||
| 	} | |||
| 	return policyDocument, err | |||
| } | |||
| 
 | |||
| func (iama *IamApiServer) CreatePolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp CreatePolicyResponse, err error) { | |||
| 	policyName := values.Get("PolicyName") | |||
| 	policyDocumentString := values.Get("PolicyDocument") | |||
| 	policyDocument, err := GetPolicyDocument(&policyDocumentString) | |||
| 	if err != nil { | |||
| 		return CreatePolicyResponse{}, err | |||
| 	} | |||
| 	policyId := Hash(&policyDocumentString) | |||
| 	arn := fmt.Sprintf("arn:aws:iam:::policy/%s", policyName) | |||
| 	resp.CreatePolicyResult.Policy.PolicyName = &policyName | |||
| 	resp.CreatePolicyResult.Policy.Arn = &arn | |||
| 	resp.CreatePolicyResult.Policy.PolicyId = &policyId | |||
| 	policyDocuments[policyName] = &policyDocument | |||
| 	return resp, nil | |||
| } | |||
| 
 | |||
| func (iama *IamApiServer) PutUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp PutUserPolicyResponse, err error) { | |||
| 	userName := values.Get("UserName") | |||
| 	policyName := values.Get("PolicyName") | |||
| 	policyDocumentString := values.Get("PolicyDocument") | |||
| 	policyDocument, err := GetPolicyDocument(&policyDocumentString) | |||
| 	if err != nil { | |||
| 		return PutUserPolicyResponse{}, err | |||
| 	} | |||
| 	policyDocuments[policyName] = &policyDocument | |||
| 	actions := GetActions(&policyDocument) | |||
| 	for _, ident := range s3cfg.Identities { | |||
| 		if userName == ident.Name { | |||
| 			for _, action := range actions { | |||
| 				ident.Actions = append(ident.Actions, action) | |||
| 			} | |||
| 			break | |||
| 		} | |||
| 	} | |||
| 	return resp, nil | |||
| } | |||
| 
 | |||
| func MapAction(action string) string { | |||
| 	switch action { | |||
| 	case "*": | |||
| 		return s3_constants.ACTION_ADMIN | |||
| 	case "Put*": | |||
| 		return s3_constants.ACTION_WRITE | |||
| 	case "Get*": | |||
| 		return s3_constants.ACTION_READ | |||
| 	case "List*": | |||
| 		return s3_constants.ACTION_LIST | |||
| 	default: | |||
| 		return s3_constants.ACTION_TAGGING | |||
| 	} | |||
| } | |||
| 
 | |||
| func GetActions(policy *PolicyDocument) (actions []string) { | |||
| 	for _, statement := range policy.Statement { | |||
| 		if statement.Effect != "Allow" { | |||
| 			continue | |||
| 		} | |||
| 		for _, resource := range statement.Resource { | |||
| 			// Parse "arn:aws:s3:::my-bucket/shared/*"
 | |||
| 			res := strings.Split(resource, ":") | |||
| 			if len(res) != 6 || res[0] != "arn" || res[1] != "aws" || res[2] != "s3" { | |||
| 				glog.Infof("not match resource: %s", res) | |||
| 				continue | |||
| 			} | |||
| 			for _, action := range statement.Action { | |||
| 				// Parse "s3:Get*"
 | |||
| 				act := strings.Split(action, ":") | |||
| 				if len(act) != 2 || act[0] != "s3" { | |||
| 					glog.Infof("not match action: %s", act) | |||
| 					continue | |||
| 				} | |||
| 				if res[5] == "*" { | |||
| 					actions = append(actions, MapAction(act[1])) | |||
| 					continue | |||
| 				} | |||
| 				// Parse my-bucket/shared/*
 | |||
| 				path := strings.Split(res[5], "/") | |||
| 				if len(path) != 2 || path[1] != "*" { | |||
| 					glog.Infof("not match bucket: %s", path) | |||
| 					continue | |||
| 				} | |||
| 				actions = append(actions, fmt.Sprintf("%s:%s", MapAction(act[1]), path[0])) | |||
| 			} | |||
| 		} | |||
| 	} | |||
| 	return actions | |||
| } | |||
| 
 | |||
| func (iama *IamApiServer) CreateAccessKey(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp CreateAccessKeyResponse) { | |||
| 	userName := values.Get("UserName") | |||
| 	status := iam.StatusTypeActive | |||
| 	accessKeyId := StringWithCharset(21, charsetUpper) | |||
| 	secretAccessKey := StringWithCharset(42, charset) | |||
| 	resp.CreateAccessKeyResult.AccessKey.AccessKeyId = &accessKeyId | |||
| 	resp.CreateAccessKeyResult.AccessKey.SecretAccessKey = &secretAccessKey | |||
| 	resp.CreateAccessKeyResult.AccessKey.UserName = &userName | |||
| 	resp.CreateAccessKeyResult.AccessKey.Status = &status | |||
| 	changed := false | |||
| 	for _, ident := range s3cfg.Identities { | |||
| 		if userName == ident.Name { | |||
| 			ident.Credentials = append(ident.Credentials, | |||
| 				&iam_pb.Credential{AccessKey: accessKeyId, SecretKey: secretAccessKey}) | |||
| 			changed = true | |||
| 			break | |||
| 		} | |||
| 	} | |||
| 	if !changed { | |||
| 		s3cfg.Identities = append(s3cfg.Identities, | |||
| 			&iam_pb.Identity{Name: userName, | |||
| 				Credentials: []*iam_pb.Credential{ | |||
| 					{ | |||
| 						AccessKey: accessKeyId, | |||
| 						SecretKey: secretAccessKey, | |||
| 					}, | |||
| 				}, | |||
| 			}, | |||
| 		) | |||
| 	} | |||
| 	return resp | |||
| } | |||
| 
 | |||
| func (iama *IamApiServer) DeleteAccessKey(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp DeleteAccessKeyResponse) { | |||
| 	userName := values.Get("UserName") | |||
| 	accessKeyId := values.Get("AccessKeyId") | |||
| 	for _, ident := range s3cfg.Identities { | |||
| 		if userName == ident.Name { | |||
| 			for i, cred := range ident.Credentials { | |||
| 				if cred.AccessKey == accessKeyId { | |||
| 					ident.Credentials = append(ident.Credentials[:i], ident.Credentials[i+1:]...) | |||
| 					break | |||
| 				} | |||
| 			} | |||
| 			break | |||
| 		} | |||
| 	} | |||
| 	return resp | |||
| } | |||
| 
 | |||
| func (iama *IamApiServer) DoActions(w http.ResponseWriter, r *http.Request) { | |||
| 	if err := r.ParseForm(); err != nil { | |||
| 		writeErrorResponse(w, s3err.ErrInvalidRequest, r.URL) | |||
| 		return | |||
| 	} | |||
| 	values := r.PostForm | |||
| 	var s3cfgLock sync.RWMutex | |||
| 	s3cfgLock.RLock() | |||
| 	s3cfg := &iam_pb.S3ApiConfiguration{} | |||
| 	if err := iama.s3ApiConfig.GetS3ApiConfiguration(s3cfg); err != nil { | |||
| 		writeErrorResponse(w, s3err.ErrInternalError, r.URL) | |||
| 		return | |||
| 	} | |||
| 	s3cfgLock.RUnlock() | |||
| 
 | |||
| 	glog.V(4).Infof("DoActions: %+v", values) | |||
| 	var response interface{} | |||
| 	var err error | |||
| 	changed := true | |||
| 	switch r.Form.Get("Action") { | |||
| 	case "ListUsers": | |||
| 		response = iama.ListUsers(s3cfg, values) | |||
| 		changed = false | |||
| 	case "ListAccessKeys": | |||
| 		response = iama.ListAccessKeys(s3cfg, values) | |||
| 		changed = false | |||
| 	case "CreateUser": | |||
| 		response = iama.CreateUser(s3cfg, values) | |||
| 	case "GetUser": | |||
| 		userName := values.Get("UserName") | |||
| 		response, err = iama.GetUser(s3cfg, userName) | |||
| 		if err != nil { | |||
| 			writeIamErrorResponse(w, err, "user", userName) | |||
| 			return | |||
| 		} | |||
| 	case "DeleteUser": | |||
| 		userName := values.Get("UserName") | |||
| 		response, err = iama.DeleteUser(s3cfg, userName) | |||
| 		if err != nil { | |||
| 			writeIamErrorResponse(w, err, "user", userName) | |||
| 			return | |||
| 		} | |||
| 	case "CreateAccessKey": | |||
| 		response = iama.CreateAccessKey(s3cfg, values) | |||
| 	case "DeleteAccessKey": | |||
| 		response = iama.DeleteAccessKey(s3cfg, values) | |||
| 	case "CreatePolicy": | |||
| 		response, err = iama.CreatePolicy(s3cfg, values) | |||
| 		if err != nil { | |||
| 			glog.Errorf("CreatePolicy:  %+v", err) | |||
| 			writeErrorResponse(w, s3err.ErrInvalidRequest, r.URL) | |||
| 			return | |||
| 		} | |||
| 	case "PutUserPolicy": | |||
| 		response, err = iama.PutUserPolicy(s3cfg, values) | |||
| 		if err != nil { | |||
| 			glog.Errorf("PutUserPolicy:  %+v", err) | |||
| 			writeErrorResponse(w, s3err.ErrInvalidRequest, r.URL) | |||
| 			return | |||
| 		} | |||
| 	default: | |||
| 		writeErrorResponse(w, s3err.ErrNotImplemented, r.URL) | |||
| 		return | |||
| 	} | |||
| 	if changed { | |||
| 		s3cfgLock.Lock() | |||
| 		err := iama.s3ApiConfig.PutS3ApiConfiguration(s3cfg) | |||
| 		s3cfgLock.Unlock() | |||
| 		if err != nil { | |||
| 			writeErrorResponse(w, s3err.ErrInternalError, r.URL) | |||
| 			return | |||
| 		} | |||
| 	} | |||
| 	writeSuccessResponseXML(w, encodeResponse(response)) | |||
| } | |||
| @ -0,0 +1,93 @@ | |||
| package iamapi | |||
| 
 | |||
| import ( | |||
| 	"encoding/xml" | |||
| 	"fmt" | |||
| 	"time" | |||
| 
 | |||
| 	"github.com/aws/aws-sdk-go/service/iam" | |||
| ) | |||
| 
 | |||
| type CommonResponse struct { | |||
| 	ResponseMetadata struct { | |||
| 		RequestId string `xml:"RequestId"` | |||
| 	} `xml:"ResponseMetadata"` | |||
| } | |||
| 
 | |||
| type ListUsersResponse struct { | |||
| 	CommonResponse | |||
| 	XMLName         xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ ListUsersResponse"` | |||
| 	ListUsersResult struct { | |||
| 		Users       []*iam.User `xml:"Users>member"` | |||
| 		IsTruncated bool        `xml:"IsTruncated"` | |||
| 	} `xml:"ListUsersResult"` | |||
| } | |||
| 
 | |||
| type ListAccessKeysResponse struct { | |||
| 	CommonResponse | |||
| 	XMLName              xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ ListAccessKeysResponse"` | |||
| 	ListAccessKeysResult struct { | |||
| 		AccessKeyMetadata []*iam.AccessKeyMetadata `xml:"AccessKeyMetadata>member"` | |||
| 		IsTruncated       bool                     `xml:"IsTruncated"` | |||
| 	} `xml:"ListAccessKeysResult"` | |||
| } | |||
| 
 | |||
| type DeleteAccessKeyResponse struct { | |||
| 	CommonResponse | |||
| 	XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ DeleteAccessKeyResponse"` | |||
| } | |||
| 
 | |||
| type CreatePolicyResponse struct { | |||
| 	CommonResponse | |||
| 	XMLName            xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ CreatePolicyResponse"` | |||
| 	CreatePolicyResult struct { | |||
| 		Policy iam.Policy `xml:"Policy"` | |||
| 	} `xml:"CreatePolicyResult"` | |||
| } | |||
| 
 | |||
| type CreateUserResponse struct { | |||
| 	CommonResponse | |||
| 	XMLName          xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ CreateUserResponse"` | |||
| 	CreateUserResult struct { | |||
| 		User iam.User `xml:"User"` | |||
| 	} `xml:"CreateUserResult"` | |||
| } | |||
| 
 | |||
| type DeleteUserResponse struct { | |||
| 	CommonResponse | |||
| 	XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ DeleteUserResponse"` | |||
| } | |||
| 
 | |||
| type GetUserResponse struct { | |||
| 	CommonResponse | |||
| 	XMLName       xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ GetUserResponse"` | |||
| 	GetUserResult struct { | |||
| 		User iam.User `xml:"User"` | |||
| 	} `xml:"GetUserResult"` | |||
| } | |||
| 
 | |||
| type CreateAccessKeyResponse struct { | |||
| 	CommonResponse | |||
| 	XMLName               xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ CreateAccessKeyResponse"` | |||
| 	CreateAccessKeyResult struct { | |||
| 		AccessKey iam.AccessKey `xml:"AccessKey"` | |||
| 	} `xml:"CreateAccessKeyResult"` | |||
| } | |||
| 
 | |||
| type PutUserPolicyResponse struct { | |||
| 	CommonResponse | |||
| 	XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ PutUserPolicyResponse"` | |||
| } | |||
| 
 | |||
| type ErrorResponse struct { | |||
| 	CommonResponse | |||
| 	XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ ErrorResponse"` | |||
| 	Error   struct { | |||
| 		iam.ErrorDetails | |||
| 		Type string `xml:"Type"` | |||
| 	} `xml:"Error"` | |||
| } | |||
| 
 | |||
| func (r *CommonResponse) SetRequestId() { | |||
| 	r.ResponseMetadata.RequestId = fmt.Sprintf("%d", time.Now().UnixNano()) | |||
| } | |||
| @ -0,0 +1,104 @@ | |||
| package iamapi | |||
| 
 | |||
| // https://docs.aws.amazon.com/cli/latest/reference/iam/list-roles.html
 | |||
| 
 | |||
| import ( | |||
| 	"bytes" | |||
| 	"fmt" | |||
| 	"github.com/chrislusf/seaweedfs/weed/filer" | |||
| 	"github.com/chrislusf/seaweedfs/weed/pb" | |||
| 	"github.com/chrislusf/seaweedfs/weed/pb/filer_pb" | |||
| 	"github.com/chrislusf/seaweedfs/weed/pb/iam_pb" | |||
| 	"github.com/chrislusf/seaweedfs/weed/wdclient" | |||
| 	"github.com/gorilla/mux" | |||
| 	"google.golang.org/grpc" | |||
| 	"net/http" | |||
| 	"strings" | |||
| ) | |||
| 
 | |||
| type IamS3ApiConfig interface { | |||
| 	GetS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) (err error) | |||
| 	PutS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) (err error) | |||
| } | |||
| 
 | |||
| type IamS3ApiConfigure struct { | |||
| 	option       *IamServerOption | |||
| 	masterClient *wdclient.MasterClient | |||
| } | |||
| 
 | |||
| type IamServerOption struct { | |||
| 	Masters          string | |||
| 	Filer            string | |||
| 	Port             int | |||
| 	FilerGrpcAddress string | |||
| 	GrpcDialOption   grpc.DialOption | |||
| } | |||
| 
 | |||
| type IamApiServer struct { | |||
| 	s3ApiConfig IamS3ApiConfig | |||
| 	filerclient *filer_pb.SeaweedFilerClient | |||
| } | |||
| 
 | |||
| var s3ApiConfigure IamS3ApiConfig | |||
| 
 | |||
| func NewIamApiServer(router *mux.Router, option *IamServerOption) (iamApiServer *IamApiServer, err error) { | |||
| 	s3ApiConfigure = IamS3ApiConfigure{ | |||
| 		option:       option, | |||
| 		masterClient: wdclient.NewMasterClient(option.GrpcDialOption, pb.AdminShellClient, "", 0, "", strings.Split(option.Masters, ",")), | |||
| 	} | |||
| 
 | |||
| 	iamApiServer = &IamApiServer{ | |||
| 		s3ApiConfig: s3ApiConfigure, | |||
| 	} | |||
| 
 | |||
| 	iamApiServer.registerRouter(router) | |||
| 
 | |||
| 	return iamApiServer, nil | |||
| } | |||
| 
 | |||
| func (iama *IamApiServer) registerRouter(router *mux.Router) { | |||
| 	// API Router
 | |||
| 	apiRouter := router.PathPrefix("/").Subrouter() | |||
| 	// ListBuckets
 | |||
| 
 | |||
| 	// apiRouter.Methods("GET").Path("/").HandlerFunc(track(s3a.iam.Auth(s3a.ListBucketsHandler, ACTION_ADMIN), "LIST"))
 | |||
| 	apiRouter.Path("/").Methods("POST").HandlerFunc(iama.DoActions) | |||
| 	// NotFound
 | |||
| 	apiRouter.NotFoundHandler = http.HandlerFunc(notFoundHandler) | |||
| } | |||
| 
 | |||
| func (iam IamS3ApiConfigure) GetS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) (err error) { | |||
| 	var buf bytes.Buffer | |||
| 	err = pb.WithGrpcFilerClient(iam.option.FilerGrpcAddress, iam.option.GrpcDialOption, func(client filer_pb.SeaweedFilerClient) error { | |||
| 		if err = filer.ReadEntry(iam.masterClient, client, filer.IamConfigDirecotry, filer.IamIdentityFile, &buf); err != nil { | |||
| 			return err | |||
| 		} | |||
| 		return nil | |||
| 	}) | |||
| 	if err != nil { | |||
| 		return err | |||
| 	} | |||
| 	if buf.Len() > 0 { | |||
| 		if err = filer.ParseS3ConfigurationFromBytes(buf.Bytes(), s3cfg); err != nil { | |||
| 			return err | |||
| 		} | |||
| 	} | |||
| 	return nil | |||
| } | |||
| 
 | |||
| func (iam IamS3ApiConfigure) PutS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) (err error) { | |||
| 	buf := bytes.Buffer{} | |||
| 	if err := filer.S3ConfigurationToText(&buf, s3cfg); err != nil { | |||
| 		return fmt.Errorf("S3ConfigurationToText: %s", err) | |||
| 	} | |||
| 	return pb.WithGrpcFilerClient( | |||
| 		iam.option.FilerGrpcAddress, | |||
| 		iam.option.GrpcDialOption, | |||
| 		func(client filer_pb.SeaweedFilerClient) error { | |||
| 			if err := filer.SaveInsideFiler(client, filer.IamConfigDirecotry, filer.IamIdentityFile, buf.Bytes()); err != nil { | |||
| 				return err | |||
| 			} | |||
| 			return nil | |||
| 		}, | |||
| 	) | |||
| } | |||
| @ -0,0 +1,157 @@ | |||
| package iamapi | |||
| 
 | |||
| import ( | |||
| 	"encoding/xml" | |||
| 	"github.com/aws/aws-sdk-go/aws" | |||
| 	"github.com/aws/aws-sdk-go/aws/session" | |||
| 	"github.com/aws/aws-sdk-go/service/iam" | |||
| 	"github.com/chrislusf/seaweedfs/weed/pb/iam_pb" | |||
| 	"github.com/gorilla/mux" | |||
| 	"github.com/stretchr/testify/assert" | |||
| 	"net/http" | |||
| 	"net/http/httptest" | |||
| 	"testing" | |||
| ) | |||
| 
 | |||
| var S3config iam_pb.S3ApiConfiguration | |||
| var GetS3ApiConfiguration func(s3cfg *iam_pb.S3ApiConfiguration) (err error) | |||
| var PutS3ApiConfiguration func(s3cfg *iam_pb.S3ApiConfiguration) (err error) | |||
| 
 | |||
| type iamS3ApiConfigureMock struct{} | |||
| 
 | |||
| func (iam iamS3ApiConfigureMock) GetS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) (err error) { | |||
| 	s3cfg = &S3config | |||
| 	return nil | |||
| } | |||
| 
 | |||
| func (iam iamS3ApiConfigureMock) PutS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) (err error) { | |||
| 	S3config = *s3cfg | |||
| 	return nil | |||
| } | |||
| 
 | |||
| var a = IamApiServer{} | |||
| 
 | |||
| func TestCreateUser(t *testing.T) { | |||
| 	userName := aws.String("Test") | |||
| 	params := &iam.CreateUserInput{UserName: userName} | |||
| 	req, _ := iam.New(session.New()).CreateUserRequest(params) | |||
| 	_ = req.Build() | |||
| 	out := CreateUserResponse{} | |||
| 	response, err := executeRequest(req.HTTPRequest, out) | |||
| 	assert.Equal(t, nil, err) | |||
| 	assert.Equal(t, http.StatusOK, response.Code) | |||
| 	//assert.Equal(t, out.XMLName, "lol")
 | |||
| } | |||
| 
 | |||
| func TestListUsers(t *testing.T) { | |||
| 	params := &iam.ListUsersInput{} | |||
| 	req, _ := iam.New(session.New()).ListUsersRequest(params) | |||
| 	_ = req.Build() | |||
| 	out := ListUsersResponse{} | |||
| 	response, err := executeRequest(req.HTTPRequest, out) | |||
| 	assert.Equal(t, nil, err) | |||
| 	assert.Equal(t, http.StatusOK, response.Code) | |||
| } | |||
| 
 | |||
| func TestListAccessKeys(t *testing.T) { | |||
| 	svc := iam.New(session.New()) | |||
| 	params := &iam.ListAccessKeysInput{} | |||
| 	req, _ := svc.ListAccessKeysRequest(params) | |||
| 	_ = req.Build() | |||
| 	out := ListAccessKeysResponse{} | |||
| 	response, err := executeRequest(req.HTTPRequest, out) | |||
| 	assert.Equal(t, nil, err) | |||
| 	assert.Equal(t, http.StatusOK, response.Code) | |||
| } | |||
| 
 | |||
| func TestDeleteUser(t *testing.T) { | |||
| 	userName := aws.String("Test") | |||
| 	params := &iam.DeleteUserInput{UserName: userName} | |||
| 	req, _ := iam.New(session.New()).DeleteUserRequest(params) | |||
| 	_ = req.Build() | |||
| 	out := DeleteUserResponse{} | |||
| 	response, err := executeRequest(req.HTTPRequest, out) | |||
| 	assert.Equal(t, nil, err) | |||
| 	assert.Equal(t, http.StatusNotFound, response.Code) | |||
| } | |||
| 
 | |||
| func TestGetUser(t *testing.T) { | |||
| 	userName := aws.String("Test") | |||
| 	params := &iam.GetUserInput{UserName: userName} | |||
| 	req, _ := iam.New(session.New()).GetUserRequest(params) | |||
| 	_ = req.Build() | |||
| 	out := GetUserResponse{} | |||
| 	response, err := executeRequest(req.HTTPRequest, out) | |||
| 	assert.Equal(t, nil, err) | |||
| 	assert.Equal(t, http.StatusNotFound, response.Code) | |||
| } | |||
| 
 | |||
| // Todo flat statement
 | |||
| func TestCreatePolicy(t *testing.T) { | |||
| 	params := &iam.CreatePolicyInput{ | |||
| 		PolicyName: aws.String("S3-read-only-example-bucket"), | |||
| 		PolicyDocument: aws.String(` | |||
| 			{ | |||
| 			  "Version": "2012-10-17", | |||
| 			  "Statement": [ | |||
| 				{ | |||
| 				  "Effect": "Allow", | |||
| 				  "Action": [ | |||
| 					"s3:Get*", | |||
| 					"s3:List*" | |||
| 				  ], | |||
| 				  "Resource": [ | |||
| 					"arn:aws:s3:::EXAMPLE-BUCKET", | |||
| 					"arn:aws:s3:::EXAMPLE-BUCKET/*" | |||
| 				  ] | |||
| 				} | |||
| 			  ] | |||
| 			}`), | |||
| 	} | |||
| 	req, _ := iam.New(session.New()).CreatePolicyRequest(params) | |||
| 	_ = req.Build() | |||
| 	out := CreatePolicyResponse{} | |||
| 	response, err := executeRequest(req.HTTPRequest, out) | |||
| 	assert.Equal(t, nil, err) | |||
| 	assert.Equal(t, http.StatusOK, response.Code) | |||
| } | |||
| 
 | |||
| func TestPutUserPolicy(t *testing.T) { | |||
| 	userName := aws.String("Test") | |||
| 	params := &iam.PutUserPolicyInput{ | |||
| 		UserName:   userName, | |||
| 		PolicyName: aws.String("S3-read-only-example-bucket"), | |||
| 		PolicyDocument: aws.String( | |||
| 			`{ | |||
| 				  "Version": "2012-10-17", | |||
| 				  "Statement": [ | |||
| 					{ | |||
| 					  "Effect": "Allow", | |||
| 					  "Action": [ | |||
| 						"s3:Get*", | |||
| 						"s3:List*" | |||
| 					  ], | |||
| 					  "Resource": [ | |||
| 						"arn:aws:s3:::EXAMPLE-BUCKET", | |||
| 						"arn:aws:s3:::EXAMPLE-BUCKET/*" | |||
| 					  ] | |||
| 					} | |||
| 				  ] | |||
| 			}`), | |||
| 	} | |||
| 	req, _ := iam.New(session.New()).PutUserPolicyRequest(params) | |||
| 	_ = req.Build() | |||
| 	out := PutUserPolicyResponse{} | |||
| 	response, err := executeRequest(req.HTTPRequest, out) | |||
| 	assert.Equal(t, nil, err) | |||
| 	assert.Equal(t, http.StatusOK, response.Code) | |||
| } | |||
| 
 | |||
| func executeRequest(req *http.Request, v interface{}) (*httptest.ResponseRecorder, error) { | |||
| 	rr := httptest.NewRecorder() | |||
| 	apiRouter := mux.NewRouter().SkipClean(true) | |||
| 	a.s3ApiConfig = iamS3ApiConfigureMock{} | |||
| 	apiRouter.Path("/").Methods("POST").HandlerFunc(a.DoActions) | |||
| 	apiRouter.ServeHTTP(rr, req) | |||
| 	return rr, xml.Unmarshal(rr.Body.Bytes(), &v) | |||
| } | |||
						Write
						Preview
					
					
					Loading…
					
					Cancel
						Save
					
		Reference in new issue