diff --git a/weed/s3api/s3acl/acl_helper.go b/weed/s3api/s3acl/acl_helper.go index 3150e4daf..350e703c4 100644 --- a/weed/s3api/s3acl/acl_helper.go +++ b/weed/s3api/s3acl/acl_helper.go @@ -32,12 +32,12 @@ func ExtractAcl(r *http.Request, accountManager *s3account.AccountManager, owner var acp s3.AccessControlPolicy err := xmlutil.UnmarshalXML(&acp, xml.NewDecoder(r.Body), "") - if err != nil { + if err != nil || acp.Owner == nil || acp.Owner.ID == nil { return nil, s3err.ErrInvalidRequest } //owner should present && owner is immutable - if acp.Owner == nil || acp.Owner.ID == nil || *acp.Owner.ID != ownerId { + if *acp.Owner.ID != ownerId { glog.V(3).Infof("set acl denied! owner account is not consistent, request account id: %s, expect account id: %s", accountId, ownerId) return nil, s3err.ErrAccessDenied } @@ -251,7 +251,7 @@ func ParseCannedAclHeader(bucketOwnership, bucketOwnerId, accountId, cannedAcl s case s3_constants.CannedAclAwsExecRead: err = s3err.ErrNotImplemented default: - err = s3err.ErrNotImplemented + err = s3err.ErrInvalidRequest } return } @@ -373,7 +373,7 @@ func SetAcpOwnerHeader(r *http.Request, acpOwnerId string) { func GetAcpOwner(entryExtended map[string][]byte, defaultOwner string) string { ownerIdBytes, ok := entryExtended[s3_constants.ExtAmzOwnerKey] - if ok { + if ok && len(ownerIdBytes) > 0 { return string(ownerIdBytes) } return defaultOwner @@ -393,7 +393,7 @@ func SetAcpGrantsHeader(r *http.Request, acpGrants []*s3.Grant) { // GetAcpGrants return grants parsed from entry func GetAcpGrants(entryExtended map[string][]byte) []*s3.Grant { acpBytes, ok := entryExtended[s3_constants.ExtAmzAclKey] - if ok { + if ok && len(acpBytes) > 0 { var grants []*s3.Grant err := json.Unmarshal(acpBytes, &grants) if err == nil { @@ -405,13 +405,23 @@ func GetAcpGrants(entryExtended map[string][]byte) []*s3.Grant { // AssembleEntryWithAcp fill entry with owner and grants func AssembleEntryWithAcp(objectEntry *filer_pb.Entry, objectOwner string, grants []*s3.Grant) s3err.ErrorCode { - objectEntry.Extended[s3_constants.ExtAmzOwnerKey] = []byte(objectOwner) - grantsBytes, err := json.Marshal(grants) - if err != nil { - glog.Warning("assemble acp to entry:", err) - return s3err.ErrInvalidRequest + if objectEntry.Extended == nil { + objectEntry.Extended = make(map[string][]byte, 0) + } + + if len(objectOwner) > 0 { + objectEntry.Extended[s3_constants.ExtAmzOwnerKey] = []byte(objectOwner) + } + + if len(grants) > 0 { + grantsBytes, err := json.Marshal(grants) + if err != nil { + glog.Warning("assemble acp to entry:", err) + return s3err.ErrInvalidRequest + } + objectEntry.Extended[s3_constants.ExtAmzAclKey] = grantsBytes } - objectEntry.Extended[s3_constants.ExtAmzAclKey] = grantsBytes + return s3err.ErrNone } diff --git a/weed/s3api/s3acl/acl_helper_test.go b/weed/s3api/s3acl/acl_helper_test.go index b58fcc171..41f719235 100644 --- a/weed/s3api/s3acl/acl_helper_test.go +++ b/weed/s3api/s3acl/acl_helper_test.go @@ -1,80 +1,534 @@ package s3acl import ( + "bytes" + "encoding/json" "github.com/aws/aws-sdk-go/service/s3" + "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" "github.com/seaweedfs/seaweedfs/weed/s3api/s3account" "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" + "io" + "net/http" "testing" ) -func TestParseAclHeaders(t *testing.T) { - accountManager := &s3account.AccountManager{ +var ( + accountManager = &s3account.AccountManager{ IdNameMapping: map[string]string{ s3account.AccountAdmin.Id: s3account.AccountAdmin.Name, s3account.AccountAnonymous.Id: s3account.AccountAnonymous.Name, + "accountA": "accountA", + "accountB": "accountB", }, EmailIdMapping: map[string]string{ s3account.AccountAdmin.EmailAddress: s3account.AccountAdmin.Id, s3account.AccountAnonymous.EmailAddress: s3account.AccountAnonymous.Id, + "accountA@example.com": "accountA", + "accountBexample.com": "accountB", }, } +) + +func TestGetAccountId(t *testing.T) { + req := &http.Request{ + Header: make(map[string][]string, 0), + } + //case1 + //accountId: "admin" + req.Header.Set(s3_constants.AmzAccountId, s3account.AccountAdmin.Id) + if GetAccountId(req) != s3account.AccountAdmin.Id { + t.Fatal("expect accountId: admin") + } + + //case2 + //accountId: "anoymous" + req.Header.Set(s3_constants.AmzAccountId, s3account.AccountAnonymous.Id) + if GetAccountId(req) != s3account.AccountAnonymous.Id { + t.Fatal("expect accountId: anonymous") + } + + //case3 + //accountId is nil => "anonymous" + req.Header.Del(s3_constants.AmzAccountId) + if GetAccountId(req) != s3account.AccountAnonymous.Id { + t.Fatal("expect accountId: anonymous") + } +} + +func TestExtractAcl(t *testing.T) { + type Case struct { + id int + resultErrCode, expectErrCode s3err.ErrorCode + resultGrants, expectGrants []*s3.Grant + } + testCases := make([]*Case, 0) + accountAdminId := "admin" + + { + //case1 (good case) + //parse acp from request body + req := &http.Request{ + Header: make(map[string][]string, 0), + } + req.Body = io.NopCloser(bytes.NewReader([]byte(` + + + admin + admin + + + + + admin + + FULL_CONTROL + + + + http://acs.amazonaws.com/groups/global/AllUsers + + FULL_CONTROL + + + + `))) + objectWriter := "accountA" + grants, errCode := ExtractAcl(req, accountManager, s3_constants.OwnershipObjectWriter, accountAdminId, accountAdminId, objectWriter) + testCases = append(testCases, &Case{ + 1, + errCode, s3err.ErrNone, + grants, []*s3.Grant{ + { + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeCanonicalUser, + ID: &accountAdminId, + }, + Permission: &s3_constants.PermissionFullControl, + }, + { + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeGroup, + URI: &s3_constants.GranteeGroupAllUsers, + }, + Permission: &s3_constants.PermissionFullControl, + }, + }, + }) + } + + { + //case2 (good case) + //parse acp from header (cannedAcl) + req := &http.Request{ + Header: make(map[string][]string, 0), + } + req.Body = nil + req.Header.Set(s3_constants.AmzCannedAcl, s3_constants.CannedAclPrivate) + objectWriter := "accountA" + grants, errCode := ExtractAcl(req, accountManager, s3_constants.OwnershipObjectWriter, accountAdminId, accountAdminId, objectWriter) + testCases = append(testCases, &Case{ + 2, + errCode, s3err.ErrNone, + grants, []*s3.Grant{ + { + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeCanonicalUser, + ID: &objectWriter, + }, + Permission: &s3_constants.PermissionFullControl, + }, + }, + }) + } + + { + //case3 (bad case) + //parse acp from request body (content is invalid) + req := &http.Request{ + Header: make(map[string][]string, 0), + } + req.Body = io.NopCloser(bytes.NewReader([]byte("zdfsaf"))) + req.Header.Set(s3_constants.AmzCannedAcl, s3_constants.CannedAclPrivate) + objectWriter := "accountA" + _, errCode := ExtractAcl(req, accountManager, s3_constants.OwnershipObjectWriter, accountAdminId, accountAdminId, objectWriter) + testCases = append(testCases, &Case{ + id: 3, + resultErrCode: errCode, expectErrCode: s3err.ErrInvalidRequest, + }) + } + + //case4 (bad case) + //parse acp from header (cannedAcl is invalid) + req := &http.Request{ + Header: make(map[string][]string, 0), + } + req.Body = nil + req.Header.Set(s3_constants.AmzCannedAcl, "dfaksjfk") + objectWriter := "accountA" + _, errCode := ExtractAcl(req, accountManager, s3_constants.OwnershipObjectWriter, accountAdminId, "", objectWriter) + testCases = append(testCases, &Case{ + id: 4, + resultErrCode: errCode, expectErrCode: s3err.ErrInvalidRequest, + }) + + { + //case5 (bad case) + //parse acp from request body: owner is inconsistent + req.Body = io.NopCloser(bytes.NewReader([]byte(` + + + admin + admin + + + + + admin + + FULL_CONTROL + + + + http://acs.amazonaws.com/groups/global/AllUsers + + FULL_CONTROL + + + + `))) + objectWriter = "accountA" + _, errCode := ExtractAcl(req, accountManager, s3_constants.OwnershipObjectWriter, accountAdminId, objectWriter, objectWriter) + testCases = append(testCases, &Case{ + id: 5, + resultErrCode: errCode, expectErrCode: s3err.ErrAccessDenied, + }) + } - //good value - grants := make([]*s3.Grant, 0) - validHeaderValue := `uri="http://acs.amazonaws.com/groups/global/AllUsers", id="anonymous", emailAddress="admin@example.com"` - errCode := ParseCustomAclHeader(validHeaderValue, s3_constants.PermissionFullControl, &grants) - if errCode != s3err.ErrNone { - t.Fatal(errCode) + for _, tc := range testCases { + if tc.resultErrCode != tc.expectErrCode { + t.Fatalf("case[%d]: errorCode not expect", tc.id) + } + if !grantsEquals(tc.resultGrants, tc.expectGrants) { + t.Fatalf("case[%d]: grants not expect", tc.id) + } } - _, errCode = ValidateAndTransferGrants(accountManager, grants) - if errCode != s3err.ErrNone { - t.Fatal(errCode) +} + +func TestParseAndValidateAclHeaders(t *testing.T) { + type Case struct { + id int + resultOwner, expectOwner string + resultErrCode, expectErrCode s3err.ErrorCode + resultGrants, expectGrants []*s3.Grant } + testCases := make([]*Case, 0) + bucketOwner := "admin" - //bad case: acl header format error - grants = make([]*s3.Grant, 0) - formatErrCase := `uri, id="anonymous", emailAddress="admin@example.com"` - errCode = ParseCustomAclHeader(formatErrCase, s3_constants.PermissionFullControl, &grants) - if errCode != s3err.ErrInvalidRequest { - t.Fatal(errCode) + { + //case1 (good case) + //parse custom acl + req := &http.Request{ + Header: make(map[string][]string, 0), + } + objectWriter := "accountA" + req.Header.Set(s3_constants.AmzAclFullControl, `uri="http://acs.amazonaws.com/groups/global/AllUsers", id="anonymous", emailAddress="admin@example.com"`) + ownerId, grants, errCode := ParseAndValidateAclHeaders(req, accountManager, s3_constants.OwnershipObjectWriter, bucketOwner, objectWriter, false) + testCases = append(testCases, &Case{ + 1, + ownerId, objectWriter, + errCode, s3err.ErrNone, + grants, []*s3.Grant{ + { + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeGroup, + URI: &s3_constants.GranteeGroupAllUsers, + }, + Permission: &s3_constants.PermissionFullControl, + }, + { + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeCanonicalUser, + ID: &s3account.AccountAnonymous.Id, + }, + Permission: &s3_constants.PermissionFullControl, + }, + { + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeCanonicalUser, + ID: &s3account.AccountAdmin.Id, + }, + Permission: &s3_constants.PermissionFullControl, + }, + }, + }) + } + { + //case2 (good case) + //parse canned acl (ownership=ObjectWriter) + req := &http.Request{ + Header: make(map[string][]string, 0), + } + objectWriter := "accountA" + req.Header.Set(s3_constants.AmzCannedAcl, s3_constants.CannedAclBucketOwnerFullControl) + ownerId, grants, errCode := ParseAndValidateAclHeaders(req, accountManager, s3_constants.OwnershipObjectWriter, bucketOwner, objectWriter, false) + testCases = append(testCases, &Case{ + 2, + ownerId, objectWriter, + errCode, s3err.ErrNone, + grants, []*s3.Grant{ + { + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeCanonicalUser, + ID: &objectWriter, + }, + Permission: &s3_constants.PermissionFullControl, + }, + { + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeCanonicalUser, + ID: &bucketOwner, + }, + Permission: &s3_constants.PermissionFullControl, + }, + }, + }) + } + { + //case3 (good case) + //parse canned acl (ownership=OwnershipBucketOwnerPreferred) + req := &http.Request{ + Header: make(map[string][]string, 0), + } + objectWriter := "accountA" + req.Header.Set(s3_constants.AmzCannedAcl, s3_constants.CannedAclBucketOwnerFullControl) + ownerId, grants, errCode := ParseAndValidateAclHeaders(req, accountManager, s3_constants.OwnershipBucketOwnerPreferred, bucketOwner, objectWriter, false) + testCases = append(testCases, &Case{ + 3, + ownerId, bucketOwner, + errCode, s3err.ErrNone, + grants, []*s3.Grant{ + { + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeCanonicalUser, + ID: &bucketOwner, + }, + Permission: &s3_constants.PermissionFullControl, + }, + }, + }) + } + { + //case4 (bad case) + //parse custom acl (grantee id not exists) + req := &http.Request{ + Header: make(map[string][]string, 0), + } + objectWriter := "accountA" + req.Header.Set(s3_constants.AmzAclFullControl, `uri="http://acs.amazonaws.com/groups/global/AllUsers", id="notExistsAccount", emailAddress="admin@example.com"`) + _, _, errCode := ParseAndValidateAclHeaders(req, accountManager, s3_constants.OwnershipObjectWriter, bucketOwner, objectWriter, false) + testCases = append(testCases, &Case{ + id: 4, + resultErrCode: errCode, expectErrCode: s3err.ErrInvalidRequest, + }) + } + + { + //case5 (bad case) + //parse custom acl (invalid format) + req := &http.Request{ + Header: make(map[string][]string, 0), + } + objectWriter := "accountA" + req.Header.Set(s3_constants.AmzAclFullControl, `uri="http:sfasf"`) + _, _, errCode := ParseAndValidateAclHeaders(req, accountManager, s3_constants.OwnershipObjectWriter, bucketOwner, objectWriter, false) + testCases = append(testCases, &Case{ + id: 5, + resultErrCode: errCode, expectErrCode: s3err.ErrInvalidRequest, + }) + } + + { + //case6 (bad case) + //parse canned acl (invalid value) + req := &http.Request{ + Header: make(map[string][]string, 0), + } + objectWriter := "accountA" + req.Header.Set(s3_constants.AmzCannedAcl, `uri="http:sfasf"`) + _, _, errCode := ParseAndValidateAclHeaders(req, accountManager, s3_constants.OwnershipObjectWriter, bucketOwner, objectWriter, false) + testCases = append(testCases, &Case{ + id: 5, + resultErrCode: errCode, expectErrCode: s3err.ErrInvalidRequest, + }) + } + + for _, tc := range testCases { + if tc.expectErrCode != tc.resultErrCode { + t.Errorf("case[%d]: errCode unexpect", tc.id) + } + if tc.resultOwner != tc.expectOwner { + t.Errorf("case[%d]: ownerId unexpect", tc.id) + } + if !grantsEquals(tc.resultGrants, tc.expectGrants) { + t.Fatalf("case[%d]: grants not expect", tc.id) + } } +} - //bad case: email not exists - grants = make([]*s3.Grant, 0) - badCaseOfEmail := `uri="http://acs.amazonaws.com/groups/global/AllUsers", id="anonymous", emailAddress="admin@example1.com"` - errCode = ParseCustomAclHeader(badCaseOfEmail, s3_constants.PermissionFullControl, &grants) - if errCode != s3err.ErrNone { - t.Fatal(errCode) +func grantsEquals(a, b []*s3.Grant) bool { + if len(a) != len(b) { + return false } - _, errCode = ValidateAndTransferGrants(accountManager, grants) - if errCode != s3err.ErrInvalidRequest { - t.Fatal(errCode) + for i, grant := range a { + if !GrantEquals(grant, b[i]) { + return false + } } + return true +} - //bad case: account id not exists - grants = make([]*s3.Grant, 0) - badCaseOfAccountId := "uri=\"http://acs.amazonaws.com/groups/global/AllUsers\", id=\"xxxxxx\", emailAddress=\"admin@example.com\"" - errCode = ParseCustomAclHeader(badCaseOfAccountId, s3_constants.PermissionFullControl, &grants) - if errCode != s3err.ErrNone { - t.Fatal(errCode) +func TestDetermineReqGrants(t *testing.T) { + { + //case1: request account is anonymous + accountId := s3account.AccountAnonymous.Id + reqPermission := s3_constants.PermissionRead + + resultGrants := DetermineReqGrants(accountId, reqPermission) + expectGrants := []*s3.Grant{ + { + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeGroup, + URI: &s3_constants.GranteeGroupAllUsers, + }, + Permission: &reqPermission, + }, + { + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeGroup, + URI: &s3_constants.GranteeGroupAllUsers, + }, + Permission: &s3_constants.PermissionFullControl, + }, + { + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeCanonicalUser, + ID: &accountId, + }, + Permission: &reqPermission, + }, + { + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeCanonicalUser, + ID: &accountId, + }, + Permission: &s3_constants.PermissionFullControl, + }, + } + if !grantsEquals(resultGrants, expectGrants) { + t.Fatalf("grants not expect") + } } - _, errCode = ValidateAndTransferGrants(accountManager, grants) - if errCode != s3err.ErrInvalidRequest { - t.Fatal(errCode) + { + //case2: request account is not anonymous (Iam authed) + accountId := "accountX" + reqPermission := s3_constants.PermissionRead + + resultGrants := DetermineReqGrants(accountId, reqPermission) + expectGrants := []*s3.Grant{ + { + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeGroup, + URI: &s3_constants.GranteeGroupAllUsers, + }, + Permission: &reqPermission, + }, + { + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeGroup, + URI: &s3_constants.GranteeGroupAllUsers, + }, + Permission: &s3_constants.PermissionFullControl, + }, + { + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeCanonicalUser, + ID: &accountId, + }, + Permission: &reqPermission, + }, + { + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeCanonicalUser, + ID: &accountId, + }, + Permission: &s3_constants.PermissionFullControl, + }, + { + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeGroup, + URI: &s3_constants.GranteeGroupAuthenticatedUsers, + }, + Permission: &reqPermission, + }, + { + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeGroup, + URI: &s3_constants.GranteeGroupAuthenticatedUsers, + }, + Permission: &s3_constants.PermissionFullControl, + }, + } + if !grantsEquals(resultGrants, expectGrants) { + t.Fatalf("grants not expect") + } } +} + +func TestAssembleEntryWithAcp(t *testing.T) { + defaultOwner := "admin" + { + //case1 + expectOwner := "accountS" + expectGrants := []*s3.Grant{ + { + Permission: &s3_constants.PermissionRead, + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeGroup, + ID: &s3account.AccountAdmin.Id, + URI: &s3_constants.GranteeGroupAllUsers, + }, + }, + } + entry := &filer_pb.Entry{} + AssembleEntryWithAcp(entry, expectOwner, expectGrants) + + resultOwner := GetAcpOwner(entry.Extended, defaultOwner) + if resultOwner != expectOwner { + t.Fatalf("owner not expect") + } - //bad case: group url not valid - grants = make([]*s3.Grant, 0) - badCaseOfURL := "uri=\"http://acs.amazonaws.com/groups/global/AllUsers111xxxx\", id=\"anonymous\", emailAddress=\"admin@example.com\"" - errCode = ParseCustomAclHeader(badCaseOfURL, s3_constants.PermissionFullControl, &grants) - if errCode != s3err.ErrNone { - t.Fatal(errCode) + resultGrants := GetAcpGrants(entry.Extended) + if !grantsEquals(resultGrants, expectGrants) { + t.Fatal("grants not expect") + } } - _, errCode = ValidateAndTransferGrants(accountManager, grants) - if errCode != s3err.ErrInvalidRequest { - t.Fatal(errCode) + { + //case2 + entry := &filer_pb.Entry{} + AssembleEntryWithAcp(entry, "", nil) + + resultOwner := GetAcpOwner(entry.Extended, defaultOwner) + if resultOwner != defaultOwner { + t.Fatalf("owner not expect") + } + + resultGrants := GetAcpGrants(entry.Extended) + if len(resultGrants) != 0 { + t.Fatal("grants not expect") + } } + } func TestGrantEquals(t *testing.T) { @@ -218,3 +672,37 @@ func TestGrantEquals(t *testing.T) { } } } + +func TestSetAcpOwnerHeader(t *testing.T) { + ownerId := "accountZ" + req := &http.Request{ + Header: make(map[string][]string, 0), + } + SetAcpOwnerHeader(req, ownerId) + + if req.Header.Get(s3_constants.ExtAmzOwnerKey) != ownerId { + t.Fatalf("owner unexpect") + } +} + +func TestSetAcpGrantsHeader(t *testing.T) { + req := &http.Request{ + Header: make(map[string][]string, 0), + } + grants := []*s3.Grant{ + { + Permission: &s3_constants.PermissionRead, + Grantee: &s3.Grantee{ + Type: &s3_constants.GrantTypeGroup, + ID: &s3account.AccountAdmin.Id, + URI: &s3_constants.GranteeGroupAllUsers, + }, + }, + } + SetAcpGrantsHeader(req, grants) + + grantsJson, _ := json.Marshal(grants) + if req.Header.Get(s3_constants.ExtAmzAclKey) != string(grantsJson) { + t.Fatalf("owner unexpect") + } +}