diff --git a/go.mod b/go.mod index 2a187d6a7..ee33926c0 100644 --- a/go.mod +++ b/go.mod @@ -42,6 +42,7 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4 github.com/grpc-ecosystem/grpc-gateway v1.11.0 // indirect github.com/jcmturner/gofork v1.0.0 // indirect + github.com/jinzhu/copier v0.2.8 github.com/json-iterator/go v1.1.10 github.com/karlseguin/ccache v2.0.3+incompatible // indirect github.com/karlseguin/ccache/v2 v2.0.7 diff --git a/go.sum b/go.sum index 043b9b0dc..52166e601 100644 --- a/go.sum +++ b/go.sum @@ -437,6 +437,8 @@ github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03 h1:FUwcHNlEqkqLjL github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8= github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jinzhu/copier v0.2.8 h1:N8MbL5niMwE3P4dOwurJixz5rMkKfujmMRFmAanSzWE= +github.com/jinzhu/copier v0.2.8/go.mod h1:24xnZezI2Yqac9J61UC6/dG/k76ttpq0DdJI3QmUvro= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= diff --git a/weed/filer/filer_conf.go b/weed/filer/filer_conf.go index 8e549f5ad..ab5afc5cc 100644 --- a/weed/filer/filer_conf.go +++ b/weed/filer/filer_conf.go @@ -18,6 +18,7 @@ const ( FilerConfName = "filer.conf" IamConfigDirecotry = "/etc/iam" IamIdentityFile = "identity.json" + IamPoliciesFile = "policies.json" ) type FilerConf struct { diff --git a/weed/iamapi/iamapi_management_handlers.go b/weed/iamapi/iamapi_management_handlers.go index 4ca3525ec..b00ada234 100644 --- a/weed/iamapi/iamapi_management_handlers.go +++ b/weed/iamapi/iamapi_management_handlers.go @@ -11,6 +11,7 @@ import ( "math/rand" "net/http" "net/url" + "reflect" "strings" "sync" "time" @@ -19,27 +20,77 @@ import ( ) const ( - charsetUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - charset = charsetUpper + "abcdefghijklmnopqrstuvwxyz/" + charsetUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + charset = charsetUpper + "abcdefghijklmnopqrstuvwxyz/" + policyDocumentVersion = "2012-10-17" + StatementActionAdmin = "*" + StatementActionWrite = "Put*" + StatementActionRead = "Get*" + StatementActionList = "List*" + StatementActionTagging = "Tagging*" ) var ( seededRand *rand.Rand = rand.New( rand.NewSource(time.Now().UnixNano())) policyDocuments = map[string]*PolicyDocument{} + policyLock = sync.RWMutex{} ) +func MapToStatementAction(action string) string { + switch action { + case StatementActionAdmin: + return s3_constants.ACTION_ADMIN + case StatementActionWrite: + return s3_constants.ACTION_WRITE + case StatementActionRead: + return s3_constants.ACTION_READ + case StatementActionList: + return s3_constants.ACTION_LIST + case StatementActionTagging: + return s3_constants.ACTION_TAGGING + default: + return "" + } +} + +func MapToIdentitiesAction(action string) string { + switch action { + case s3_constants.ACTION_ADMIN: + return StatementActionAdmin + case s3_constants.ACTION_WRITE: + return StatementActionWrite + case s3_constants.ACTION_READ: + return StatementActionRead + case s3_constants.ACTION_LIST: + return StatementActionList + case s3_constants.ACTION_TAGGING: + return StatementActionTagging + default: + return "" + } +} + type Statement struct { Effect string `json:"Effect"` Action []string `json:"Action"` Resource []string `json:"Resource"` } +type Policies struct { + Policies map[string]PolicyDocument `json:"policies"` +} + type PolicyDocument struct { Version string `json:"Version"` Statement []*Statement `json:"Statement"` } +func (p PolicyDocument) String() string { + b, _ := json.Marshal(p) + return string(b) +} + func Hash(s *string) string { h := sha1.New() h.Write([]byte(*s)) @@ -83,7 +134,7 @@ func (iama *IamApiServer) CreateUser(s3cfg *iam_pb.S3ApiConfiguration, values ur 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:]...) + s3cfg.Identities = append(s3cfg.Identities[:i], s3cfg.Identities[i+1:]...) return resp, nil } } @@ -119,7 +170,16 @@ func (iama *IamApiServer) CreatePolicy(s3cfg *iam_pb.S3ApiConfiguration, values resp.CreatePolicyResult.Policy.PolicyName = &policyName resp.CreatePolicyResult.Policy.Arn = &arn resp.CreatePolicyResult.Policy.PolicyId = &policyId - policyDocuments[policyName] = &policyDocument + policies := Policies{} + policyLock.Lock() + defer policyLock.Unlock() + if err = iama.s3ApiConfig.GetPolicies(&policies); err != nil { + return resp, err + } + policies.Policies[policyName] = policyDocument + if err = iama.s3ApiConfig.PutPolicies(&policies); err != nil { + return resp, err + } return resp, nil } @@ -144,6 +204,60 @@ func (iama *IamApiServer) PutUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values return resp, nil } +func (iama *IamApiServer) GetUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp GetUserPolicyResponse, err error) { + userName := values.Get("UserName") + policyName := values.Get("PolicyName") + for _, ident := range s3cfg.Identities { + if userName != ident.Name { + continue + } + + resp.GetUserPolicyResult.UserName = userName + resp.GetUserPolicyResult.PolicyName = policyName + if len(ident.Actions) == 0 { + return resp, fmt.Errorf(iam.ErrCodeNoSuchEntityException) + } + + policyDocument := PolicyDocument{Version: policyDocumentVersion} + statements := make(map[string][]string) + for _, action := range ident.Actions { + // parse "Read:EXAMPLE-BUCKET" + act := strings.Split(action, ":") + + resource := "*" + if len(act) == 2 { + resource = fmt.Sprintf("arn:aws:s3:::%s/*", act[1]) + } + statements[resource] = append(statements[resource], + fmt.Sprintf("s3:%s", MapToIdentitiesAction(act[0])), + ) + } + for resource, actions := range statements { + isEqAction := false + for i, statement := range policyDocument.Statement { + if reflect.DeepEqual(statement.Action, actions) { + policyDocument.Statement[i].Resource = append( + policyDocument.Statement[i].Resource, resource) + isEqAction = true + break + } + } + if isEqAction { + continue + } + policyDocumentStatement := Statement{ + Effect: "Allow", + Action: actions, + } + policyDocumentStatement.Resource = append(policyDocumentStatement.Resource, resource) + policyDocument.Statement = append(policyDocument.Statement, &policyDocumentStatement) + } + resp.GetUserPolicyResult.PolicyDocument = policyDocument.String() + return resp, nil + } + return resp, fmt.Errorf(iam.ErrCodeNoSuchEntityException) +} + func (iama *IamApiServer) DeleteUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, values url.Values) (resp PutUserPolicyResponse, err error) { userName := values.Get("UserName") for i, ident := range s3cfg.Identities { @@ -155,21 +269,6 @@ func (iama *IamApiServer) DeleteUserPolicy(s3cfg *iam_pb.S3ApiConfiguration, val return resp, fmt.Errorf(iam.ErrCodeNoSuchEntityException) } -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" { @@ -189,8 +288,9 @@ func GetActions(policy *PolicyDocument) (actions []string) { glog.Infof("not match action: %s", act) continue } + statementAction := MapToStatementAction(act[1]) if res[5] == "*" { - actions = append(actions, MapAction(act[1])) + actions = append(actions, statementAction) continue } // Parse my-bucket/shared/* @@ -199,7 +299,7 @@ func GetActions(policy *PolicyDocument) (actions []string) { glog.Infof("not match bucket: %s", path) continue } - actions = append(actions, fmt.Sprintf("%s:%s", MapAction(act[1]), path[0])) + actions = append(actions, fmt.Sprintf("%s:%s", statementAction, path[0])) } } } @@ -291,6 +391,7 @@ func (iama *IamApiServer) DoActions(w http.ResponseWriter, r *http.Request) { writeIamErrorResponse(w, err, "user", userName, nil) return } + changed = false case "DeleteUser": userName := values.Get("UserName") response, err = iama.DeleteUser(s3cfg, userName) @@ -316,6 +417,13 @@ func (iama *IamApiServer) DoActions(w http.ResponseWriter, r *http.Request) { writeErrorResponse(w, s3err.ErrInvalidRequest, r.URL) return } + case "GetUserPolicy": + response, err = iama.GetUserPolicy(s3cfg, values) + if err != nil { + writeIamErrorResponse(w, err, "user", values.Get("UserName"), nil) + return + } + changed = false case "DeleteUserPolicy": if response, err = iama.DeleteUserPolicy(s3cfg, values); err != nil { writeIamErrorResponse(w, err, "user", values.Get("UserName"), nil) diff --git a/weed/iamapi/iamapi_response.go b/weed/iamapi/iamapi_response.go index 26dd0f263..77328b608 100644 --- a/weed/iamapi/iamapi_response.go +++ b/weed/iamapi/iamapi_response.go @@ -79,6 +79,16 @@ type PutUserPolicyResponse struct { XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ PutUserPolicyResponse"` } +type GetUserPolicyResponse struct { + CommonResponse + XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ GetUserPolicyResponse"` + GetUserPolicyResult struct { + UserName string `xml:"UserName"` + PolicyName string `xml:"PolicyName"` + PolicyDocument string `xml:"PolicyDocument"` + } `xml:"GetUserPolicyResult"` +} + type ErrorResponse struct { CommonResponse XMLName xml.Name `xml:"https://iam.amazonaws.com/doc/2010-05-08/ ErrorResponse"` diff --git a/weed/iamapi/iamapi_server.go b/weed/iamapi/iamapi_server.go index d0a033ed6..18af1a919 100644 --- a/weed/iamapi/iamapi_server.go +++ b/weed/iamapi/iamapi_server.go @@ -4,6 +4,7 @@ package iamapi import ( "bytes" + "encoding/json" "fmt" "github.com/chrislusf/seaweedfs/weed/filer" "github.com/chrislusf/seaweedfs/weed/pb" @@ -21,6 +22,8 @@ import ( type IamS3ApiConfig interface { GetS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) (err error) PutS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) (err error) + GetPolicies(policies *Policies) (err error) + PutPolicies(policies *Policies) (err error) } type IamS3ApiConfigure struct { @@ -106,3 +109,41 @@ func (iam IamS3ApiConfigure) PutS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfigurat }, ) } + +func (iam IamS3ApiConfigure) GetPolicies(policies *Policies) (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.IamPoliciesFile, &buf); err != nil { + return err + } + return nil + }) + if err != nil { + return err + } + if buf.Len() == 0 { + policies.Policies = make(map[string]PolicyDocument) + return nil + } + if err := json.Unmarshal(buf.Bytes(), policies); err != nil { + return err + } + return nil +} + +func (iam IamS3ApiConfigure) PutPolicies(policies *Policies) (err error) { + var b []byte + if b, err = json.Marshal(policies); err != nil { + return err + } + return pb.WithGrpcFilerClient( + iam.option.FilerGrpcAddress, + iam.option.GrpcDialOption, + func(client filer_pb.SeaweedFilerClient) error { + if err := filer.SaveInsideFiler(client, filer.IamConfigDirecotry, filer.IamPoliciesFile, b); err != nil { + return err + } + return nil + }, + ) +} diff --git a/weed/iamapi/iamapi_test.go b/weed/iamapi/iamapi_test.go index f989626e6..09aaf0ac8 100644 --- a/weed/iamapi/iamapi_test.go +++ b/weed/iamapi/iamapi_test.go @@ -7,29 +7,43 @@ import ( "github.com/aws/aws-sdk-go/service/iam" "github.com/chrislusf/seaweedfs/weed/pb/iam_pb" "github.com/gorilla/mux" + "github.com/jinzhu/copier" "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) +var GetPolicies func(policies *Policies) (err error) +var PutPolicies func(policies *Policies) (err error) + +var s3config = iam_pb.S3ApiConfiguration{} +var policiesFile = Policies{Policies: make(map[string]PolicyDocument)} +var ias = IamApiServer{s3ApiConfig: iamS3ApiConfigureMock{}} type iamS3ApiConfigureMock struct{} func (iam iamS3ApiConfigureMock) GetS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) (err error) { - s3cfg = &S3config + _ = copier.Copy(&s3cfg.Identities, &s3config.Identities) return nil } func (iam iamS3ApiConfigureMock) PutS3ApiConfiguration(s3cfg *iam_pb.S3ApiConfiguration) (err error) { - S3config = *s3cfg + _ = copier.Copy(&s3config.Identities, &s3cfg.Identities) return nil } -var a = IamApiServer{} +func (iam iamS3ApiConfigureMock) GetPolicies(policies *Policies) (err error) { + _ = copier.Copy(&policies, &policiesFile) + return nil +} + +func (iam iamS3ApiConfigureMock) PutPolicies(policies *Policies) (err error) { + _ = copier.Copy(&policiesFile, &policies) + return nil +} func TestCreateUser(t *testing.T) { userName := aws.String("Test") @@ -64,17 +78,6 @@ func TestListAccessKeys(t *testing.T) { 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} @@ -83,7 +86,7 @@ func TestGetUser(t *testing.T) { out := GetUserResponse{} response, err := executeRequest(req.HTTPRequest, out) assert.Equal(t, nil, err) - assert.Equal(t, http.StatusNotFound, response.Code) + assert.Equal(t, http.StatusOK, response.Code) } // Todo flat statement @@ -147,11 +150,32 @@ func TestPutUserPolicy(t *testing.T) { assert.Equal(t, http.StatusOK, response.Code) } +func TestGetUserPolicy(t *testing.T) { + userName := aws.String("Test") + params := &iam.GetUserPolicyInput{UserName: userName, PolicyName: aws.String("S3-read-only-example-bucket")} + req, _ := iam.New(session.New()).GetUserPolicyRequest(params) + _ = req.Build() + out := GetUserPolicyResponse{} + 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.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.Path("/").Methods("POST").HandlerFunc(ias.DoActions) apiRouter.ServeHTTP(rr, req) return rr, xml.Unmarshal(rr.Body.Bytes(), &v) }