diff --git a/bearer/bearer_test.go b/bearer/bearer_test.go index c226ba76..ec88daa7 100644 --- a/bearer/bearer_test.go +++ b/bearer/bearer_test.go @@ -264,6 +264,7 @@ func TestToken_Sign(t *testing.T) { val2 := bearertest.Token() require.NoError(t, val2.Unmarshal(val.Marshal())) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/606") require.True(t, val2.VerifySignature()) jd, err := val.MarshalJSON() diff --git a/client/container_statistic_test.go b/client/container_statistic_test.go index 537079b4..6c884484 100644 --- a/client/container_statistic_test.go +++ b/client/container_statistic_test.go @@ -112,14 +112,8 @@ func testEaclTable(containerID cid.ID) eacl.Table { var table eacl.Table table.SetCID(containerID) - r := eacl.NewRecord() - r.SetOperation(eacl.OperationPut) - r.SetAction(eacl.ActionAllow) - - var target eacl.Target - target.SetRole(eacl.RoleOthers) - r.SetTargets(target) - table.AddRecord(r) + r := eacl.ConstructRecord(eacl.ActionAllow, eacl.OperationPut, []eacl.Target{eacl.NewTargetByRole(eacl.RoleOthers)}) + table.AddRecord(&r) return table } diff --git a/eacl/common_test.go b/eacl/common_test.go new file mode 100644 index 00000000..3fd1c096 --- /dev/null +++ b/eacl/common_test.go @@ -0,0 +1,366 @@ +package eacl_test + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/sha256" + "math/big" + "testing" + + protoacl "github.com/nspcc-dev/neofs-api-go/v2/acl" + "github.com/nspcc-dev/neofs-sdk-go/checksum" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/eacl" + "github.com/nspcc-dev/neofs-sdk-go/object" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + "github.com/nspcc-dev/neofs-sdk-go/user" + "github.com/nspcc-dev/neofs-sdk-go/version" + "github.com/nspcc-dev/tzhash/tz" + "github.com/stretchr/testify/require" +) + +// Crypto. +var ( + anyECDSAPublicKeys = []ecdsa.PublicKey{ + {Curve: elliptic.P256(), + X: new(big.Int).SetBytes([]byte{202, 217, 142, 98, 209, 190, 188, 145, 123, 174, 21, 173, 239, 239, 245, 67, 148, 205, 119, 58, 223, 219, 209, 220, 113, 215, 134, 228, 101, 249, 34, 218}), + Y: new(big.Int).SetBytes([]byte{16, 170, 6, 224, 77, 3, 245, 72, 144, 58, 69, 14, 160, 35, 57, 108, 111, 27, 224, 129, 88, 230, 68, 48, 17, 10, 207, 118, 199, 120, 184, 119}), + }, + {Curve: elliptic.P256(), + X: new(big.Int).SetBytes([]byte{149, 43, 50, 196, 91, 177, 62, 131, 233, 126, 241, 177, 13, 78, 96, 94, 119, 71, 55, 179, 8, 53, 241, 79, 2, 1, 95, 85, 78, 45, 197, 136}), + Y: new(big.Int).SetBytes([]byte{118, 201, 238, 8, 178, 41, 96, 3, 163, 197, 31, 58, 106, 218, 104, 47, 106, 153, 180, 68, 109, 243, 62, 31, 159, 17, 104, 134, 134, 97, 117, 52}), + }, + } + // // corresponds to anyECDSAPublicKeys. + anyECDSAPublicKeysPtr []*ecdsa.PublicKey // set by init + // corresponds to anyECDSAPublicKeys. + anyValidECDSABinPublicKeys = [][]byte{ + {3, 202, 217, 142, 98, 209, 190, 188, 145, 123, 174, 21, 173, 239, 239, 245, 67, 148, 205, 119, 58, 223, 219, 209, 220, 113, 215, 134, 228, 101, 249, 34, 218}, + {2, 149, 43, 50, 196, 91, 177, 62, 131, 233, 126, 241, 177, 13, 78, 96, 94, 119, 71, 55, 179, 8, 53, 241, 79, 2, 1, 95, 85, 78, 45, 197, 136}, + } +) + +// Other NeoFS stuff. +var ( + anyValidObjectID = oid.ID{86, 149, 134, 57, 161, 211, 240, 124, 106, 146, 201, 140, 249, 50, 158, 38, 82, 140, 5, 160, 180, 117, 106, 214, 47, 255, 166, 89, 55, 99, 178, 66} + anyValidObjectIDString = "6pzJXAjxTH6i38Yk9dFWAPY6wrUpSLi4DUwZf82EompD" + anyValidProtoVersion = version.New(4835, 1532621) + anyValidProtoVersionString = "v4835.1532621" + anyValidContainerID = cid.ID{243, 245, 75, 198, 48, 107, 141, 121, 255, 49, 51, 168, 21, 254, 62, 66, 6, 147, 43, 35, 99, 242, 163, 20, 26, 30, 147, 240, 79, 114, 252, 227} + anyValidContainerIDString = "HRK1fwY2PMS4mgy292eZeDBr5XyH6mHbXzx7ds3n81vz" + anyValidUserID = user.ID{53, 52, 176, 7, 17, 140, 220, 179, 170, 128, 138, 214, 130, 87, 179, 211, 36, 197, 16, 38, 50, 88, 207, 120, 145} + anyValidUserIDString = "NQiZCLuU4EeP47mMFW6FWEu7Pfm4MDFVBn" + anyData = []byte("Hello, world!") + anyUserSet = []user.ID{ + {53, 121, 249, 124, 139, 174, 30, 193, 143, 226, 163, 208, 188, 194, 173, 123, 60, 84, 224, 229, 4, 14, 206, 19, 117}, + {53, 246, 34, 60, 106, 147, 200, 106, 111, 144, 9, 61, 86, 46, 111, 148, 91, 65, 206, 216, 139, 168, 188, 23, 102}, + {53, 164, 213, 123, 6, 115, 90, 134, 224, 150, 72, 192, 236, 220, 188, 131, 102, 5, 152, 164, 166, 222, 119, 72, 228}, + } + anyValidChecksums = []checksum.Checksum{ + checksum.New(0, anyData), + checksum.New(4893983, anyData), + checksum.NewSHA256(sha256.Sum256(anyData)), + checksum.NewTillichZemor(tz.Sum(anyData)), + } + // corresponds to anyValidChecksums. + anyValidStringChecksums = []string{ + "CHECKSUM_TYPE_UNSPECIFIED:48656c6c6f2c20776f726c6421", + "4893983:48656c6c6f2c20776f726c6421", + "SHA256:315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3", + "TZ:0000014249f10795c0240eddca8a6ebf000001c9c4dc98b017fd92ad62979c8c0000008d94cd98a457b983e937838dcd000000dbc8689e75c7dd8925ad0df727", + } + anyValidObjectTypes = []object.Type{ + 32852, + object.TypeRegular, + object.TypeTombstone, + object.TypeStorageGroup, + object.TypeLock, + object.TypeLink, + } + anyValidStringObjectTypes = []string{ + "32852", + "REGULAR", + "TOMBSTONE", + "STORAGE_GROUP", + "LOCK", + "LINK", + } +) + +// EACL. +var ( + // not const to avoid separate block. + anyValidMatcher = eacl.Match(548643430) + anyValidHeaderType = eacl.FilterHeaderType(40968380) + anyValidRole = eacl.Role(690857412) + anyValidAction = eacl.Action(9875285) + anyValidOp = eacl.Operation(3462843) + anyValidBinPublicKeys = [][]byte{ + []byte("key_1940340825"), + []byte("key_879439723842"), + } + anyValidFilters = []eacl.Filter{ + eacl.ConstructFilter(eacl.FilterHeaderType(4509681), "key_54093643", eacl.Match(949385), "val_34811040"), + eacl.ConstructFilter(eacl.FilterHeaderType(582984), "key_1298432", eacl.Match(7539428), "val_8243258"), + } + anyValidTargets = []eacl.Target{ + eacl.NewTargetByRole(anyValidRole), + {}, // set by init + } + anyValidRecords = []eacl.Record{ + eacl.ConstructRecord(eacl.Action(5692342), eacl.Operation(12943052), []eacl.Target{anyValidTargets[0]}, anyValidFilters[0]), + eacl.ConstructRecord(eacl.Action(43658603), eacl.Operation(12943052), anyValidTargets, anyValidFilters...), + } + anyValidEACL = eacl.NewTableForContainer(anyValidContainerID, anyValidRecords) +) + +func init() { + rawSubjs := [][]byte{ + anyValidECDSABinPublicKeys[0], + anyUserSet[0][:], + anyValidECDSABinPublicKeys[1], + anyUserSet[1][:], + anyUserSet[2][:], + } + anyValidTargets[1].SetRawSubjects(rawSubjs) +} + +// Protobuf. +var ( + // corresponds to anyValidFilters. + anyValidBinFilters = [][]byte{ + {8, 241, 159, 147, 2, 16, 137, 249, 57, 26, 12, 107, 101, 121, 95, 53, 52, 48, 57, 51, 54, 52, 51, 34, 12, 118, 97, 108, 95, 51, 52, 56, 49, 49, 48, 52, 48}, + {8, 200, 202, 35, 16, 228, 149, 204, 3, 26, 11, 107, 101, 121, 95, 49, 50, 57, 56, 52, 51, 50, 34, 11, 118, 97, 108, 95, 56, 50, 52, 51, 50, 53, 56}, + } + // corresponds to anyValidTargets. + anyValidBinTargets = [][]byte{ + {8, 196, 203, 182, 201, 2}, + {18, 33, 3, 202, 217, 142, 98, 209, 190, 188, 145, 123, 174, 21, 173, 239, 239, 245, 67, 148, 205, 119, 58, 223, 219, + 209, 220, 113, 215, 134, 228, 101, 249, 34, 218, 18, 25, 53, 121, 249, 124, 139, 174, 30, 193, 143, 226, 163, + 208, 188, 194, 173, 123, 60, 84, 224, 229, 4, 14, 206, 19, 117, 18, 33, 2, 149, 43, 50, 196, 91, 177, 62, 131, 233, + 126, 241, 177, 13, 78, 96, 94, 119, 71, 55, 179, 8, 53, 241, 79, 2, 1, 95, 85, 78, 45, 197, 136, 18, 25, 53, 246, 34, + 60, 106, 147, 200, 106, 111, 144, 9, 61, 86, 46, 111, 148, 91, 65, 206, 216, 139, 168, 188, 23, 102, 18, 25, 53, 164, + 213, 123, 6, 115, 90, 134, 224, 150, 72, 192, 236, 220, 188, 131, 102, 5, 152, 164, 166, 222, 119, 72, 228}, + } + // corresponds to anyValidRecords. + anyValidBinRecords = [][]byte{ + {8, 204, 253, 149, 6, 16, 182, 183, 219, 2, 26, 37, 8, 241, 159, 147, 2, 16, 137, 249, 57, 26, 12, 107, 101, 121, 95, 53, + 52, 48, 57, 51, 54, 52, 51, 34, 12, 118, 97, 108, 95, 51, 52, 56, 49, 49, 48, 52, 48, 34, 6, 8, 196, 203, 182, 201, 2}, + {8, 204, 253, 149, 6, 16, 235, 218, 232, 20, 26, 37, 8, 241, 159, 147, 2, 16, 137, 249, 57, 26, 12, 107, 101, 121, 95, 53, + 52, 48, 57, 51, 54, 52, 51, 34, 12, 118, 97, 108, 95, 51, 52, 56, 49, 49, 48, 52, 48, 26, 35, 8, 200, 202, 35, 16, + 228, 149, 204, 3, 26, 11, 107, 101, 121, 95, 49, 50, 57, 56, 52, 51, 50, 34, 11, 118, 97, 108, 95, 56, 50, 52, 51, 50, + 53, 56, 34, 6, 8, 196, 203, 182, 201, 2, 34, 151, 1, 18, 33, 3, 202, 217, 142, 98, 209, 190, 188, 145, 123, 174, 21, + 173, 239, 239, 245, 67, 148, 205, 119, 58, 223, 219, 209, 220, 113, 215, 134, 228, 101, 249, 34, 218, 18, 25, 53, + 121, 249, 124, 139, 174, 30, 193, 143, 226, 163, 208, 188, 194, 173, 123, 60, 84, 224, 229, 4, 14, 206, 19, 117, 18, + 33, 2, 149, 43, 50, 196, 91, 177, 62, 131, 233, 126, 241, 177, 13, 78, 96, 94, 119, 71, 55, 179, 8, 53, 241, 79, 2, 1, 95, + 85, 78, 45, 197, 136, 18, 25, 53, 246, 34, 60, 106, 147, 200, 106, 111, 144, 9, 61, 86, 46, 111, 148, 91, 65, 206, 216, 139, + 168, 188, 23, 102, 18, 25, 53, 164, 213, 123, 6, 115, 90, 134, 224, 150, 72, 192, 236, 220, 188, 131, 102, 5, 152, + 164, 166, 222, 119, 72, 228}, + } + anyValidEACLBytes = []byte{10, 4, 8, 2, 16, 16, 18, 34, 10, 32, 243, 245, 75, 198, 48, 107, 141, 121, 255, 49, 51, 168, 21, 254, + 62, 66, 6, 147, 43, 35, 99, 242, 163, 20, 26, 30, 147, 240, 79, 114, 252, 227, 26, 57, 8, 204, 253, 149, 6, 16, 182, 183, + 219, 2, 26, 37, 8, 241, 159, 147, 2, 16, 137, 249, 57, 26, 12, 107, 101, 121, 95, 53, 52, 48, 57, 51, 54, 52, 51, 34, 12, 118, + 97, 108, 95, 51, 52, 56, 49, 49, 48, 52, 48, 34, 6, 8, 196, 203, 182, 201, 2, 26, 248, 1, 8, 204, 253, 149, 6, 16, 235, + 218, 232, 20, 26, 37, 8, 241, 159, 147, 2, 16, 137, 249, 57, 26, 12, 107, 101, 121, 95, 53, 52, 48, 57, 51, 54, 52, 51, + 34, 12, 118, 97, 108, 95, 51, 52, 56, 49, 49, 48, 52, 48, 26, 35, 8, 200, 202, 35, 16, 228, 149, 204, 3, 26, 11, 107, + 101, 121, 95, 49, 50, 57, 56, 52, 51, 50, 34, 11, 118, 97, 108, 95, 56, 50, 52, 51, 50, 53, 56, 34, 6, 8, 196, 203, 182, 201, + 2, 34, 151, 1, 18, 33, 3, 202, 217, 142, 98, 209, 190, 188, 145, 123, 174, 21, 173, 239, 239, 245, 67, 148, 205, 119, 58, + 223, 219, 209, 220, 113, 215, 134, 228, 101, 249, 34, 218, 18, 25, 53, 121, 249, 124, 139, 174, 30, 193, 143, 226, 163, + 208, 188, 194, 173, 123, 60, 84, 224, 229, 4, 14, 206, 19, 117, 18, 33, 2, 149, 43, 50, 196, 91, 177, 62, 131, 233, 126, + 241, 177, 13, 78, 96, 94, 119, 71, 55, 179, 8, 53, 241, 79, 2, 1, 95, 85, 78, 45, 197, 136, 18, 25, 53, 246, 34, 60, 106, 147, + 200, 106, 111, 144, 9, 61, 86, 46, 111, 148, 91, 65, 206, 216, 139, 168, 188, 23, 102, 18, 25, 53, 164, 213, 123, 6, 115, 90, + 134, 224, 150, 72, 192, 236, 220, 188, 131, 102, 5, 152, 164, 166, 222, 119, 72, 228} +) + +// Protojson. +var ( + // corresponds to anyValidFilters. + anyValidJSONFilters = []string{` +{ + "headerType": 4509681, + "matchType": 949385, + "key": "key_54093643", + "value": "val_34811040" +} +`, ` +{ + "headerType": 582984, + "matchType": 7539428, + "key": "key_1298432", + "value": "val_8243258" +} +`} + // corresponds to anyValidTargets. + anyValidJSONTargets = []string{` +{ + "role": 690857412, + "keys": [] +} +`, ` +{ + "role": "ROLE_UNSPECIFIED", + "keys": [ + "A8rZjmLRvryRe64Vre/v9UOUzXc639vR3HHXhuRl+SLa", + "NXn5fIuuHsGP4qPQvMKtezxU4OUEDs4TdQ==", + "ApUrMsRbsT6D6X7xsQ1OYF53RzezCDXxTwIBX1VOLcWI", + "NfYiPGqTyGpvkAk9Vi5vlFtBztiLqLwXZg==", + "NaTVewZzWobglkjA7Ny8g2YFmKSm3ndI5A==" + ] +} +`} + // corresponds to anyValidRecords. + anyValidJSONRecords = []string{` +{ + "operation": 5692342, + "action": 12943052, + "filters": [ + { + "headerType": 4509681, + "matchType": 949385, + "key": "key_54093643", + "value": "val_34811040" + } + ], + "targets": [ + { + "role": 690857412, + "keys": [] + } + ] +} +`, ` +{ + "operation": 43658603, + "action": 12943052, + "filters": [ + { + "headerType": 4509681, + "matchType": 949385, + "key": "key_54093643", + "value": "val_34811040" + }, + { + "headerType": 582984, + "matchType": 7539428, + "key": "key_1298432", + "value": "val_8243258" + } + ], + "targets": [ + { + "role": 690857412, + "keys": [] + }, + { + "role": "ROLE_UNSPECIFIED", + "keys": [ + "A8rZjmLRvryRe64Vre/v9UOUzXc639vR3HHXhuRl+SLa", + "NXn5fIuuHsGP4qPQvMKtezxU4OUEDs4TdQ==", + "ApUrMsRbsT6D6X7xsQ1OYF53RzezCDXxTwIBX1VOLcWI", + "NfYiPGqTyGpvkAk9Vi5vlFtBztiLqLwXZg==", + "NaTVewZzWobglkjA7Ny8g2YFmKSm3ndI5A==" + ] + } + ] +} +`} + anyValidEACLJSON = ` +{ + "version": { + "major": 2, + "minor": 16 + }, + "containerID": { + "value": "8/VLxjBrjXn/MTOoFf4+QgaTKyNj8qMUGh6T8E9y/OM=" + }, + "records": [ + { + "operation": 5692342, + "action": 12943052, + "filters": [ + { + "headerType": 4509681, + "matchType": 949385, + "key": "key_54093643", + "value": "val_34811040" + } + ], + "targets": [ + { + "role": 690857412, + "keys": [] + } + ] + }, + { + "operation": 43658603, + "action": 12943052, + "filters": [ + { + "headerType": 43658603, + "matchType": 949385, + "key": "key_54093643", + "value": "val_34811040" + }, + { + "headerType": 582984, + "matchType": 7539428, + "key": "key_1298432", + "value": "val_8243258" + } + ], + "targets": [ + { + "role": 690857412, + "keys": [] + }, + { + "role": "ROLE_UNSPECIFIED", + "keys": [ + "A8rZjmLRvryRe64Vre/v9UOUzXc639vR3HHXhuRl+SLa", + "NXn5fIuuHsGP4qPQvMKtezxU4OUEDs4TdQ==", + "ApUrMsRbsT6D6X7xsQ1OYF53RzezCDXxTwIBX1VOLcWI", + "NfYiPGqTyGpvkAk9Vi5vlFtBztiLqLwXZg==", + "NaTVewZzWobglkjA7Ny8g2YFmKSm3ndI5A==" + ] + } + ] + } + ] +} +` +) + +func init() { + for i := range anyECDSAPublicKeys { + anyECDSAPublicKeysPtr = append(anyECDSAPublicKeysPtr, &anyECDSAPublicKeys[i]) + } +} + +func assertProtoTargetsEqual(t testing.TB, ts []eacl.Target, ms []protoacl.Target) { + require.Len(t, ms, len(ts)) + for i := range ts { + require.EqualValues(t, ts[i].Role(), ms[i].GetRole(), i) + require.Equal(t, ts[i].RawSubjects(), ms[i].GetKeys(), i) + } +} + +func assertProtoFiltersEqual(t testing.TB, fs []eacl.Filter, ms []protoacl.HeaderFilter) { + require.Len(t, ms, len(fs)) + for i := range fs { + require.EqualValues(t, fs[i].From(), ms[i].GetHeaderType(), i) + require.Equal(t, fs[i].Key(), ms[i].GetKey(), i) + require.EqualValues(t, fs[i].Matcher(), ms[i].GetMatchType(), i) + require.Equal(t, fs[i].Value(), ms[i].GetValue(), i) + } +} + +func assertProtoRecordsEqual(t testing.TB, rs []eacl.Record, ms []protoacl.Record) { + require.Len(t, ms, len(rs)) + for i := range rs { + require.EqualValues(t, rs[i].Action(), ms[i].GetAction(), i) + require.EqualValues(t, rs[i].Operation(), ms[i].GetOperation(), i) + assertProtoTargetsEqual(t, rs[i].Targets(), ms[i].GetTargets()) + assertProtoFiltersEqual(t, rs[i].Filters(), ms[i].GetFilters()) + } +} diff --git a/eacl/enums.go b/eacl/enums.go index 6ced5a07..f961bdb5 100644 --- a/eacl/enums.go +++ b/eacl/enums.go @@ -1,145 +1,144 @@ package eacl import ( + "strconv" + v2acl "github.com/nspcc-dev/neofs-api-go/v2/acl" ) -// Action taken if ContainerEACL record matched request. -// Action is compatible with v2 acl.Action enum. +// Action enumerates actions that may be applied within NeoFS access management. +// What and how specific Action affects depends on the specific context. type Action uint32 const ( - // ActionUnknown is an Action value used to mark action as undefined. - ActionUnknown Action = iota - - // ActionAllow is an Action value that allows access to the operation from context. - ActionAllow - - // ActionDeny is an Action value that denies access to the operation from context. - ActionDeny + ActionUnspecified Action = iota // undefined (zero) + ActionAllow // allow the op + ActionDeny // deny the op ) -// Operation is a object service method to match request. -// Operation is compatible with v2 acl.Operation enum. +// ActionUnknown is an Action value used to mark action as undefined. +// Deprecated: use ActionUnspecified instead. +const ActionUnknown = ActionUnspecified + +// Operation enumerates operations on NeoFS resources under access control. type Operation uint32 const ( - // OperationUnknown is an Operation value used to mark operation as undefined. - OperationUnknown Operation = iota - - // OperationGet is an object get Operation. - OperationGet - - // OperationHead is an Operation of getting the object header. - OperationHead - - // OperationPut is an object put Operation. - OperationPut - - // OperationDelete is an object delete Operation. - OperationDelete - - // OperationSearch is an object search Operation. - OperationSearch - - // OperationRange is an object payload range retrieval Operation. - OperationRange - - // OperationRangeHash is an object payload range hashing Operation. - OperationRangeHash + OperationUnspecified Operation = iota // undefined (zero) + OperationGet // ObjectService.Get RPC + OperationHead // ObjectService.Head RPC + OperationPut // ObjectService.Put RPC + OperationDelete // ObjectService.Delete RPC + OperationSearch // ObjectService.Search RPC + OperationRange // ObjectService.GetRange RPC + OperationRangeHash // ObjectService.GetRangeHash RPC ) -// Role is a group of request senders to match request. -// Role is compatible with v2 acl.Role enum. +// OperationUnknown is an Operation value used to mark operation as undefined. +// Deprecated: use OperationUnspecified instead. +const OperationUnknown = OperationUnspecified + +// Role enumerates groups of subjects requesting access to NeoFS resources. type Role uint32 const ( - // RoleUnknown is a Role value used to mark role as undefined. - RoleUnknown Role = iota - - // RoleUser is a group of senders that contains only key of container owner. - RoleUser - - // RoleSystem is a group of senders that contains keys of container nodes and - // inner ring nodes. - RoleSystem - - // RoleOthers is a group of senders that contains none of above keys. - RoleOthers + RoleUnspecified Role = iota // undefined (zero) + RoleUser // owner of the container requesting its objects + RoleSystem // Deprecated: NeoFS storage and Inner Ring nodes + RoleOthers // any other party ) -// Match is binary operation on filer name and value to check if request is matched. -// Match is compatible with v2 acl.MatchType enum. +// RoleUnknown is a Role value used to mark role as undefined. +// Deprecated: use RoleUnspecified instead. +const RoleUnknown = RoleUnspecified + +// Match enumerates operators to check attribute value compliance. What and how +// specific Match affects depends on the specific context. type Match uint32 const ( - // MatchUnknown is a Match value used to mark matcher as undefined. - MatchUnknown Match = iota - - // MatchStringEqual is a Match of string equality. - MatchStringEqual - - // MatchStringNotEqual is a Match of string inequality. - MatchStringNotEqual - - // MatchNotPresent is an operator for attribute absence. - MatchNotPresent - - // MatchNumGT is a numeric "greater than" operator. - MatchNumGT - - // MatchNumGE is a numeric "greater or equal than" operator. - MatchNumGE - - // MatchNumLT is a numeric "less than" operator. - MatchNumLT - - // MatchNumLE is a numeric "less or equal than" operator. - MatchNumLE + MatchUnspecified Match = iota // undefined (zero) + MatchStringEqual // string equality + MatchStringNotEqual // string inequality + MatchNotPresent // attribute absence + MatchNumGT // numeric "greater than" operator + MatchNumGE // numeric "greater or equal than" operator + MatchNumLT // is a numeric "less than" operator + MatchNumLE // is a numeric "less or equal than" operator ) -// FilterHeaderType indicates source of headers to make matches. -// FilterHeaderType is compatible with v2 acl.HeaderType enum. +// MatchUnknown is a Match value used to mark matcher as undefined. +// Deprecated: use MatchUnspecified instead. +const MatchUnknown = MatchUnspecified + +// FilterHeaderType enumerates the classes of resource attributes processed +// within NeoFS access management. type FilterHeaderType uint32 const ( - // HeaderTypeUnknown is a FilterHeaderType value used to mark header type as undefined. - HeaderTypeUnknown FilterHeaderType = iota + HeaderTypeUnspecified FilterHeaderType = iota // undefined (zero) + HeaderFromRequest // protocol request X-Header + HeaderFromObject // object attribute + HeaderFromService // custom application-level attribute +) + +// HeaderTypeUnknown is a FilterHeaderType value used to mark header type as undefined. +// Deprecated: use HeaderTypeUnspecified instead. +const HeaderTypeUnknown = HeaderTypeUnspecified - // HeaderFromRequest is a FilterHeaderType for request X-Header. - HeaderFromRequest +// ToV2 converts Action to v2 Action enum value. +// Deprecated: do not use it. +func (a Action) ToV2() v2acl.Action { return v2acl.Action(a) } - // HeaderFromObject is a FilterHeaderType for object header. - HeaderFromObject +// ActionFromV2 converts v2 Action enum value to Action. +// Deprecated: do not use it. +func ActionFromV2(action v2acl.Action) Action { return Action(action) } - // HeaderFromService is a FilterHeaderType for service header. - HeaderFromService +const ( + actionStringZero = "ACTION_UNSPECIFIED" + actionStringAllow = "ALLOW" + actionStringDeny = "DENY" ) -// ToV2 converts Action to v2 Action enum value. -func (a Action) ToV2() v2acl.Action { +// ActionToString maps Action values to strings: +// - 0: ACTION_UNSPECIFIED +// - [ActionAllow]: ALLOW +// - [ActionDeny]: DENY +// +// All other values are base-10 integers. +// +// The mapping is consistent and resilient to lib updates. At the same time, +// please note that this is not a NeoFS protocol format. Use [Action.String] to +// get any human-readable text for printing. +func ActionToString(a Action) string { switch a { + default: + return strconv.FormatUint(uint64(a), 10) + case 0: + return actionStringZero case ActionAllow: - return v2acl.ActionAllow + return actionStringAllow case ActionDeny: - return v2acl.ActionDeny - default: - return v2acl.ActionUnknown + return actionStringDeny } } -// ActionFromV2 converts v2 Action enum value to Action. -func ActionFromV2(action v2acl.Action) (a Action) { - switch action { - case v2acl.ActionAllow: - a = ActionAllow - case v2acl.ActionDeny: - a = ActionDeny +// ActionFromString maps strings to Action values in reverse to +// [ActionToString]. Returns false if s is incorrect. +func ActionFromString(s string) (Action, bool) { + switch s { default: - a = ActionUnknown + if n, err := strconv.ParseUint(s, 10, 32); err == nil { + return Action(n), true + } + return 0, false + case "ACTION_UNSPECIFIED": + return 0, true + case "ALLOW": + return ActionAllow, true + case "DENY": + return ActionDeny, true } - - return a } // EncodeToString returns string representation of Action. @@ -147,80 +146,116 @@ func ActionFromV2(action v2acl.Action) (a Action) { // String mapping: // - ActionAllow: ALLOW; // - ActionDeny: DENY; -// - ActionUnknown, default: ACTION_UNSPECIFIED. -func (a Action) EncodeToString() string { - return a.ToV2().String() -} +// - ActionUnspecified, default: ACTION_UNSPECIFIED. +// +// Deprecated: use [ActionToString] instead. +func (a Action) EncodeToString() string { return ActionToString(a) } // String implements fmt.Stringer. // // String is designed to be human-readable, and its format MAY differ between -// SDK versions. String MAY return same result as EncodeToString. String MUST NOT -// be used to encode ID into NeoFS protocol string. +// SDK versions. Use [ActionToString] and [ActionFromString] for consistent +// mapping. func (a Action) String() string { - return a.EncodeToString() + return ActionToString(a) } // DecodeString parses Action from a string representation. // It is a reverse action to EncodeToString(). // // Returns true if s was parsed successfully. +// Deprecated: use [ActionFromString] instead. func (a *Action) DecodeString(s string) bool { - var g v2acl.Action - - ok := g.FromString(s) - - if ok { - *a = ActionFromV2(g) + if v, ok := ActionFromString(s); ok { + *a = v + return true } - - return ok + return false } // ToV2 converts Operation to v2 Operation enum value. -func (o Operation) ToV2() v2acl.Operation { - switch o { +// Deprecated: do not use it. +func (o Operation) ToV2() v2acl.Operation { return v2acl.Operation(o) } + +// OperationFromV2 converts v2 Operation enum value to Operation. +// Deprecated: do not use it. +func OperationFromV2(operation v2acl.Operation) Operation { return Operation(operation) } + +const ( + opStringZero = "OPERATION_UNSPECIFIED" + opStringGet = "GET" + opStringHead = "HEAD" + opStringPut = "PUT" + opStringDelete = "DELETE" + opStringSearch = "SEARCH" + opStringRange = "GETRANGE" + opStringRangeHash = "GETRANGEHASH" +) + +// OperationToString maps Operation values to strings: +// - 0: OPERATION_UNSPECIFIED +// - [OperationGet]: GET +// - [OperationHead]: HEAD +// - [OperationPut]: PUT +// - [OperationDelete]: DELETE +// - [OperationSearch]: SEARCH +// - [OperationRange]: GETRANGE +// - [OperationRangeHash]: GETRANGEHASH +// +// All other values are base-10 integers. +// +// The mapping is consistent and resilient to lib updates. At the same time, +// please note that this is not a NeoFS protocol format. Use [Operation.String] to +// get any human-readable text for printing. +func OperationToString(op Operation) string { + switch op { + default: + return strconv.FormatUint(uint64(op), 10) + case 0: + return opStringZero case OperationGet: - return v2acl.OperationGet + return opStringGet case OperationHead: - return v2acl.OperationHead + return opStringHead case OperationPut: - return v2acl.OperationPut + return opStringPut case OperationDelete: - return v2acl.OperationDelete + return opStringDelete case OperationSearch: - return v2acl.OperationSearch + return opStringSearch case OperationRange: - return v2acl.OperationRange + return opStringRange case OperationRangeHash: - return v2acl.OperationRangeHash - default: - return v2acl.OperationUnknown + return opStringRangeHash } } -// OperationFromV2 converts v2 Operation enum value to Operation. -func OperationFromV2(operation v2acl.Operation) (o Operation) { - switch operation { - case v2acl.OperationGet: - o = OperationGet - case v2acl.OperationHead: - o = OperationHead - case v2acl.OperationPut: - o = OperationPut - case v2acl.OperationDelete: - o = OperationDelete - case v2acl.OperationSearch: - o = OperationSearch - case v2acl.OperationRange: - o = OperationRange - case v2acl.OperationRangeHash: - o = OperationRangeHash +// OperationFromString maps strings to Operation values in reverse to +// [OperationToString]. Returns false if s is incorrect. +func OperationFromString(s string) (Operation, bool) { + switch s { default: - o = OperationUnknown + if n, err := strconv.ParseUint(s, 10, 32); err == nil { + return Operation(n), true + } + return 0, false + case "OPERATION_UNSPECIFIED": + return 0, true + case opStringGet: + return OperationGet, true + case opStringHead: + return OperationHead, true + case opStringPut: + return OperationPut, true + case opStringDelete: + return OperationDelete, true + case opStringSearch: + return OperationSearch, true + case opStringRange: + return OperationRange, true + case opStringRangeHash: + return OperationRangeHash, true } - - return o } // EncodeToString returns string representation of Operation. @@ -233,64 +268,92 @@ func OperationFromV2(operation v2acl.Operation) (o Operation) { // - OperationSearch: SEARCH; // - OperationRange: GETRANGE; // - OperationRangeHash: GETRANGEHASH; -// - OperationUnknown, default: OPERATION_UNSPECIFIED. -func (o Operation) EncodeToString() string { - return o.ToV2().String() -} +// - OperationUnspecified, default: OPERATION_UNSPECIFIED. +// +// Deprecated: use [OperationToString] instead. +func (o Operation) EncodeToString() string { return OperationToString(o) } // String implements fmt.Stringer. // // String is designed to be human-readable, and its format MAY differ between -// SDK versions. String MAY return same result as EncodeToString. String MUST NOT -// be used to encode ID into NeoFS protocol string. +// SDK versions. Use [OperationToString] and [OperationFromString] for +// consistent mapping. func (o Operation) String() string { - return o.EncodeToString() + return OperationToString(o) } // DecodeString parses Operation from a string representation. // It is a reverse action to EncodeToString(). // // Returns true if s was parsed successfully. +// Deprecated: use [OperationFromString] instead. func (o *Operation) DecodeString(s string) bool { - var g v2acl.Operation - - ok := g.FromString(s) - - if ok { - *o = OperationFromV2(g) + if v, ok := OperationFromString(s); ok { + *o = v + return true } - - return ok + return false } // ToV2 converts Role to v2 Role enum value. -func (r Role) ToV2() v2acl.Role { +// Deprecated: do not use it. +func (r Role) ToV2() v2acl.Role { return v2acl.Role(r) } + +// RoleFromV2 converts v2 Role enum value to Role. +// Deprecated: do not use it. +func RoleFromV2(role v2acl.Role) Role { return Role(role) } + +const ( + roleStringZero = "ROLE_UNSPECIFIED" + roleStringUser = "USER" + roleStringSystem = "SYSTEM" + roleStringOthers = "OTHERS" +) + +// RoleToString maps Role values to strings: +// - 0: ROLE_UNSPECIFIED +// - [RoleUser]: USER +// - [RoleSystem]: SYSTEM +// - [RoleOthers]: OTHERS +// +// All other values are base-10 integers. +// +// The mapping is consistent and resilient to lib updates. At the same time, +// please note that this is not a NeoFS protocol format. Use [Role.String] to +// get any human-readable text for printing. +func RoleToString(r Role) string { switch r { + default: + return strconv.FormatUint(uint64(r), 10) + case 0: + return roleStringZero case RoleUser: - return v2acl.RoleUser + return roleStringUser case RoleSystem: - return v2acl.RoleSystem + return roleStringSystem case RoleOthers: - return v2acl.RoleOthers - default: - return v2acl.RoleUnknown + return roleStringOthers } } -// RoleFromV2 converts v2 Role enum value to Role. -func RoleFromV2(role v2acl.Role) (r Role) { - switch role { - case v2acl.RoleUser: - r = RoleUser - case v2acl.RoleSystem: - r = RoleSystem - case v2acl.RoleOthers: - r = RoleOthers +// RoleFromString maps strings to Role values in reverse to [RoleToString]. +// Returns false if s is incorrect. +func RoleFromString(s string) (Role, bool) { + switch s { default: - r = RoleUnknown + if n, err := strconv.ParseUint(s, 10, 32); err == nil { + return Role(n), true + } + return 0, false + case "ROLE_UNSPECIFIED": + return 0, true + case roleStringUser: + return RoleUser, true + case roleStringSystem: + return RoleSystem, true + case roleStringOthers: + return RoleOthers, true } - - return r } // EncodeToString returns string representation of Role. @@ -299,67 +362,114 @@ func RoleFromV2(role v2acl.Role) (r Role) { // - RoleUser: USER; // - RoleSystem: SYSTEM; // - RoleOthers: OTHERS; -// - RoleUnknown, default: ROLE_UNKNOWN. -func (r Role) EncodeToString() string { - return r.ToV2().String() -} +// - RoleUnspecified, default: ROLE_UNKNOWN. +// +// Deprecated: use [RoleToString] instead. +func (r Role) EncodeToString() string { return RoleToString(r) } // String implements fmt.Stringer. // // String is designed to be human-readable, and its format MAY differ between -// SDK versions. String MAY return same result as EncodeToString. String MUST NOT -// be used to encode ID into NeoFS protocol string. +// SDK versions. Use [RoleToString] and [RoleFromString] for consistent mapping. func (r Role) String() string { - return r.EncodeToString() + return RoleToString(r) } // DecodeString parses Role from a string representation. // It is a reverse action to EncodeToString(). // // Returns true if s was parsed successfully. +// Deprecated: use [RoleFromString] instead. func (r *Role) DecodeString(s string) bool { - var g v2acl.Role - - ok := g.FromString(s) - - if ok { - *r = RoleFromV2(g) + if v, ok := RoleFromString(s); ok { + *r = v + return true } - - return ok + return false } // ToV2 converts Match to v2 MatchType enum value. -func (m Match) ToV2() v2acl.MatchType { +// Deprecated: do not use it. +func (m Match) ToV2() v2acl.MatchType { return v2acl.MatchType(m) } + +// MatchFromV2 converts v2 MatchType enum value to Match. +// Deprecated: do not use it. +func MatchFromV2(match v2acl.MatchType) Match { return Match(match) } + +const ( + matcherStringZero = "MATCH_TYPE_UNSPECIFIED" + matcherStringEqual = "STRING_EQUAL" + matcherStringNotEqual = "STRING_NOT_EQUAL" + matcherStringNotPresent = "NOT_PRESENT" + matcherStringNumGT = "NUM_GT" + matcherStringNumGE = "NUM_GE" + matcherStringNumLT = "NUM_LT" + matcherStringNumLE = "NUM_LE" +) + +// MatcherToString maps Match values to strings: +// - 0: MATCH_TYPE_UNSPECIFIED +// - [MatchStringEqual]: STRING_EQUAL +// - [MatchStringNotEqual]: STRING_NOT_EQUAL +// - [MatchNotPresent]: NOT_PRESENT +// - [MatchNumGT]: NUM_GT +// - [MatchNumGE]: NUM_GE +// - [MatchNumLT]: NUM_LT +// - [MatchNumLE]: NUM_LE +// +// All other values are base-10 integers. +// +// The mapping is consistent and resilient to lib updates. At the same time, +// please note that this is not a NeoFS protocol format. Use [Match.String] to +// get any human-readable text for printing. +func MatcherToString(m Match) string { switch m { - case - MatchStringEqual, - MatchStringNotEqual, - MatchNotPresent, - MatchNumGT, - MatchNumGE, - MatchNumLT, - MatchNumLE: - return v2acl.MatchType(m) default: - return v2acl.MatchTypeUnknown + return strconv.FormatUint(uint64(m), 10) + case 0: + return matcherStringZero + case MatchStringEqual: + return matcherStringEqual + case MatchStringNotEqual: + return matcherStringNotEqual + case MatchNotPresent: + return matcherStringNotPresent + case MatchNumGT: + return matcherStringNumGT + case MatchNumGE: + return matcherStringNumGE + case MatchNumLT: + return matcherStringNumLT + case MatchNumLE: + return matcherStringNumLE } } -// MatchFromV2 converts v2 MatchType enum value to Match. -func MatchFromV2(match v2acl.MatchType) Match { - switch match { - case - v2acl.MatchTypeStringEqual, - v2acl.MatchTypeStringNotEqual, - v2acl.MatchTypeNotPresent, - v2acl.MatchTypeNumGT, - v2acl.MatchTypeNumGE, - v2acl.MatchTypeNumLT, - v2acl.MatchTypeNumLE: - return Match(match) +// MatcherFromString maps strings to Match values in reverse to +// [MatcherToString]. Returns false if s is incorrect. +func MatcherFromString(s string) (Match, bool) { + switch s { default: - return MatchUnknown + if n, err := strconv.ParseUint(s, 10, 32); err == nil { + return Match(n), true + } + return 0, false + case "MATCH_TYPE_UNSPECIFIED": + return 0, true + case matcherStringEqual: + return MatchStringEqual, true + case matcherStringNotEqual: + return MatchStringNotEqual, true + case matcherStringNotPresent: + return MatchNotPresent, true + case matcherStringNumGT: + return MatchNumGT, true + case matcherStringNumGE: + return MatchNumGE, true + case matcherStringNumLT: + return MatchNumLT, true + case matcherStringNumLE: + return MatchNumLE, true } } @@ -373,64 +483,94 @@ func MatchFromV2(match v2acl.MatchType) Match { // - MatchNumGE: NUM_GE; // - MatchNumLT: NUM_LT; // - MatchNumLE: NUM_LE; -// - MatchUnknown, default: MATCH_TYPE_UNSPECIFIED. -func (m Match) EncodeToString() string { - return m.ToV2().String() -} +// - MatchUnspecified, default: MATCH_TYPE_UNSPECIFIED. +// +// Deprecated: use [MatcherToString] instead. +func (m Match) EncodeToString() string { return MatcherToString(m) } // String implements fmt.Stringer. // // String is designed to be human-readable, and its format MAY differ between -// SDK versions. String MAY return same result as EncodeToString. String MUST NOT -// be used to encode ID into NeoFS protocol string. +// SDK versions. Use [MatcherToString] and [MatcherFromString] for consistent +// mapping. func (m Match) String() string { - return m.EncodeToString() + return MatcherToString(m) } // DecodeString parses Match from a string representation. // It is a reverse action to EncodeToString(). // // Returns true if s was parsed successfully. +// Deprecated: use [MatcherFromString] instead. func (m *Match) DecodeString(s string) bool { - var g v2acl.MatchType - - ok := g.FromString(s) - - if ok { - *m = MatchFromV2(g) + if v, ok := MatcherFromString(s); ok { + *m = v + return true } - - return ok + return false } // ToV2 converts FilterHeaderType to v2 HeaderType enum value. -func (h FilterHeaderType) ToV2() v2acl.HeaderType { +// Deprecated: do not use it. +func (h FilterHeaderType) ToV2() v2acl.HeaderType { return v2acl.HeaderType(h) } + +// FilterHeaderTypeFromV2 converts v2 HeaderType enum value to FilterHeaderType. +// Deprecated: do not use it. +func FilterHeaderTypeFromV2(header v2acl.HeaderType) FilterHeaderType { + return FilterHeaderType(header) +} + +const ( + headerTypeStringZero = "HEADER_UNSPECIFIED" + headerTypeStringRequest = "REQUEST" + headerTypeStringObject = "OBJECT" + headerTypeStringService = "SERVICE" +) + +// HeaderTypeToString maps FilterHeaderType values to strings: +// - 0: HEADER_UNSPECIFIED +// - [HeaderFromRequest]: REQUEST +// - [HeaderFromObject]: OBJECT +// - [HeaderFromService]: SERVICE +// +// All other values are base-10 integers. +// +// The mapping is consistent and resilient to lib updates. At the same time, +// please note that this is not a NeoFS protocol format. Use +// [FilterHeaderType.String] to get any human-readable text for printing. +func HeaderTypeToString(h FilterHeaderType) string { switch h { + default: + return strconv.FormatUint(uint64(h), 10) + case 0: + return headerTypeStringZero case HeaderFromRequest: - return v2acl.HeaderTypeRequest + return headerTypeStringRequest case HeaderFromObject: - return v2acl.HeaderTypeObject + return headerTypeStringObject case HeaderFromService: - return v2acl.HeaderTypeService - default: - return v2acl.HeaderTypeUnknown + return headerTypeStringService } } -// FilterHeaderTypeFromV2 converts v2 HeaderType enum value to FilterHeaderType. -func FilterHeaderTypeFromV2(header v2acl.HeaderType) (h FilterHeaderType) { - switch header { - case v2acl.HeaderTypeRequest: - h = HeaderFromRequest - case v2acl.HeaderTypeObject: - h = HeaderFromObject - case v2acl.HeaderTypeService: - h = HeaderFromService +// HeaderTypeFromString maps strings to FilterHeaderType values in reverse to +// [MatcherToString]. Returns false if s is incorrect. +func HeaderTypeFromString(s string) (FilterHeaderType, bool) { + switch s { default: - h = HeaderTypeUnknown + if n, err := strconv.ParseUint(s, 10, 32); err == nil { + return FilterHeaderType(n), true + } + return 0, false + case "HEADER_UNSPECIFIED": + return 0, true + case headerTypeStringRequest: + return HeaderFromRequest, true + case headerTypeStringObject: + return HeaderFromObject, true + case headerTypeStringService: + return HeaderFromService, true } - - return h } // EncodeToString returns string representation of FilterHeaderType. @@ -438,32 +578,29 @@ func FilterHeaderTypeFromV2(header v2acl.HeaderType) (h FilterHeaderType) { // String mapping: // - HeaderFromRequest: REQUEST; // - HeaderFromObject: OBJECT; -// - HeaderTypeUnknown, default: HEADER_UNSPECIFIED. -func (h FilterHeaderType) EncodeToString() string { - return h.ToV2().String() -} +// - HeaderTypeUnspecified, default: HEADER_UNSPECIFIED. +// +// Deprecated: use [HeaderTypeToString] instead. +func (h FilterHeaderType) EncodeToString() string { return HeaderTypeToString(h) } // String implements fmt.Stringer. // // String is designed to be human-readable, and its format MAY differ between -// SDK versions. String MAY return same result as EncodeToString. String MUST NOT -// be used to encode ID into NeoFS protocol string. +// SDK versions. Use [HeaderTypeToString] and [HeaderTypeFromString] for +// consistent mapping. func (h FilterHeaderType) String() string { - return h.EncodeToString() + return HeaderTypeToString(h) } // DecodeString parses FilterHeaderType from a string representation. // It is a reverse action to EncodeToString(). // // Returns true if s was parsed successfully. +// Deprecated: use [HeaderTypeFromString] instead. func (h *FilterHeaderType) DecodeString(s string) bool { - var g v2acl.HeaderType - - ok := g.FromString(s) - - if ok { - *h = FilterHeaderTypeFromV2(g) + if v, ok := HeaderTypeFromString(s); ok { + *h = v + return true } - - return ok + return false } diff --git a/eacl/enums_test.go b/eacl/enums_test.go index d5d95086..c5f75bff 100644 --- a/eacl/enums_test.go +++ b/eacl/enums_test.go @@ -10,31 +10,31 @@ import ( var ( eqV2Actions = map[eacl.Action]v2acl.Action{ - eacl.ActionUnknown: v2acl.ActionUnknown, - eacl.ActionAllow: v2acl.ActionAllow, - eacl.ActionDeny: v2acl.ActionDeny, + eacl.ActionUnspecified: v2acl.ActionUnknown, + eacl.ActionAllow: v2acl.ActionAllow, + eacl.ActionDeny: v2acl.ActionDeny, } eqV2Operations = map[eacl.Operation]v2acl.Operation{ - eacl.OperationUnknown: v2acl.OperationUnknown, - eacl.OperationGet: v2acl.OperationGet, - eacl.OperationHead: v2acl.OperationHead, - eacl.OperationPut: v2acl.OperationPut, - eacl.OperationDelete: v2acl.OperationDelete, - eacl.OperationSearch: v2acl.OperationSearch, - eacl.OperationRange: v2acl.OperationRange, - eacl.OperationRangeHash: v2acl.OperationRangeHash, + eacl.OperationUnspecified: v2acl.OperationUnknown, + eacl.OperationGet: v2acl.OperationGet, + eacl.OperationHead: v2acl.OperationHead, + eacl.OperationPut: v2acl.OperationPut, + eacl.OperationDelete: v2acl.OperationDelete, + eacl.OperationSearch: v2acl.OperationSearch, + eacl.OperationRange: v2acl.OperationRange, + eacl.OperationRangeHash: v2acl.OperationRangeHash, } eqV2Roles = map[eacl.Role]v2acl.Role{ - eacl.RoleUnknown: v2acl.RoleUnknown, - eacl.RoleUser: v2acl.RoleUser, - eacl.RoleSystem: v2acl.RoleSystem, - eacl.RoleOthers: v2acl.RoleOthers, + eacl.RoleUnspecified: v2acl.RoleUnknown, + eacl.RoleUser: v2acl.RoleUser, + eacl.RoleSystem: v2acl.RoleSystem, + eacl.RoleOthers: v2acl.RoleOthers, } eqV2Matches = map[eacl.Match]v2acl.MatchType{ - eacl.MatchUnknown: v2acl.MatchTypeUnknown, + eacl.MatchUnspecified: v2acl.MatchTypeUnknown, eacl.MatchStringEqual: v2acl.MatchTypeStringEqual, eacl.MatchStringNotEqual: v2acl.MatchTypeStringNotEqual, eacl.MatchNotPresent: v2acl.MatchTypeNotPresent, @@ -45,80 +45,127 @@ var ( } eqV2HeaderTypes = map[eacl.FilterHeaderType]v2acl.HeaderType{ - eacl.HeaderTypeUnknown: v2acl.HeaderTypeUnknown, - eacl.HeaderFromRequest: v2acl.HeaderTypeRequest, - eacl.HeaderFromObject: v2acl.HeaderTypeObject, - eacl.HeaderFromService: v2acl.HeaderTypeService, + eacl.HeaderTypeUnspecified: v2acl.HeaderTypeUnknown, + eacl.HeaderFromRequest: v2acl.HeaderTypeRequest, + eacl.HeaderFromObject: v2acl.HeaderTypeObject, + eacl.HeaderFromService: v2acl.HeaderTypeService, + } + + actionStrings = map[eacl.Action]string{ + 0: "ACTION_UNSPECIFIED", + eacl.ActionAllow: "ALLOW", + eacl.ActionDeny: "DENY", + 3: "3", + } + roleStrings = map[eacl.Role]string{ + 0: "ROLE_UNSPECIFIED", + eacl.RoleUser: "USER", + eacl.RoleSystem: "SYSTEM", + eacl.RoleOthers: "OTHERS", + 4: "4", + } + opStrings = map[eacl.Operation]string{ + 0: "OPERATION_UNSPECIFIED", + eacl.OperationGet: "GET", + eacl.OperationHead: "HEAD", + eacl.OperationPut: "PUT", + eacl.OperationDelete: "DELETE", + eacl.OperationSearch: "SEARCH", + eacl.OperationRange: "GETRANGE", + eacl.OperationRangeHash: "GETRANGEHASH", + 8: "8", + } + matcherStrings = map[eacl.Match]string{ + 0: "MATCH_TYPE_UNSPECIFIED", + eacl.MatchStringEqual: "STRING_EQUAL", + eacl.MatchStringNotEqual: "STRING_NOT_EQUAL", + eacl.MatchNotPresent: "NOT_PRESENT", + eacl.MatchNumGT: "NUM_GT", + eacl.MatchNumGE: "NUM_GE", + eacl.MatchNumLT: "NUM_LT", + eacl.MatchNumLE: "NUM_LE", + 8: "8", + } + headerTypeStrings = map[eacl.FilterHeaderType]string{ + 0: "HEADER_UNSPECIFIED", + eacl.HeaderFromRequest: "REQUEST", + eacl.HeaderFromObject: "OBJECT", + eacl.HeaderFromService: "SERVICE", + 4: "4", } ) func TestAction(t *testing.T) { + require.Equal(t, eacl.ActionUnspecified, eacl.ActionUnknown) t.Run("known actions", func(t *testing.T) { - for i := eacl.ActionUnknown; i <= eacl.ActionDeny; i++ { + for i := eacl.ActionUnspecified; i <= eacl.ActionDeny; i++ { require.Equal(t, eqV2Actions[i], i.ToV2()) require.Equal(t, eacl.ActionFromV2(i.ToV2()), i) } }) t.Run("unknown actions", func(t *testing.T) { - require.Equal(t, (eacl.ActionDeny + 1).ToV2(), v2acl.ActionUnknown) - require.Equal(t, eacl.ActionFromV2(v2acl.ActionDeny+1), eacl.ActionUnknown) + require.EqualValues(t, 1000, eacl.Action(1000).ToV2()) + require.EqualValues(t, 1000, eacl.ActionFromV2(1000)) }) } func TestOperation(t *testing.T) { + require.Equal(t, eacl.OperationUnspecified, eacl.OperationUnknown) t.Run("known operations", func(t *testing.T) { - for i := eacl.OperationUnknown; i <= eacl.OperationRangeHash; i++ { + for i := eacl.OperationUnspecified; i <= eacl.OperationRangeHash; i++ { require.Equal(t, eqV2Operations[i], i.ToV2()) require.Equal(t, eacl.OperationFromV2(i.ToV2()), i) } }) t.Run("unknown operations", func(t *testing.T) { - require.Equal(t, (eacl.OperationRangeHash + 1).ToV2(), v2acl.OperationUnknown) - require.Equal(t, eacl.OperationFromV2(v2acl.OperationRangeHash+1), eacl.OperationUnknown) + require.EqualValues(t, 1000, eacl.Operation(1000).ToV2()) + require.EqualValues(t, 1000, eacl.OperationFromV2(1000)) }) } func TestRole(t *testing.T) { t.Run("known roles", func(t *testing.T) { - for i := eacl.RoleUnknown; i <= eacl.RoleOthers; i++ { + for i := eacl.RoleUnspecified; i <= eacl.RoleOthers; i++ { require.Equal(t, eqV2Roles[i], i.ToV2()) require.Equal(t, eacl.RoleFromV2(i.ToV2()), i) } }) t.Run("unknown roles", func(t *testing.T) { - require.Equal(t, (eacl.RoleOthers + 1).ToV2(), v2acl.RoleUnknown) - require.Equal(t, eacl.RoleFromV2(v2acl.RoleOthers+1), eacl.RoleUnknown) + require.EqualValues(t, 1000, eacl.Operation(1000).ToV2()) + require.EqualValues(t, 1000, eacl.RoleFromV2(1000)) }) } func TestMatch(t *testing.T) { + require.Equal(t, eacl.MatchUnspecified, eacl.MatchUnknown) t.Run("known matches", func(t *testing.T) { - for i := eacl.MatchUnknown; i <= eacl.MatchStringNotEqual; i++ { + for i := eacl.MatchUnspecified; i <= eacl.MatchStringNotEqual; i++ { require.Equal(t, eqV2Matches[i], i.ToV2()) require.Equal(t, eacl.MatchFromV2(i.ToV2()), i) } }) t.Run("unknown matches", func(t *testing.T) { - require.Equal(t, (eacl.MatchNumLE + 1).ToV2(), v2acl.MatchTypeUnknown) - require.Equal(t, eacl.MatchFromV2(v2acl.MatchTypeNumLE+1), eacl.MatchUnknown) + require.EqualValues(t, 1000, eacl.Match(1000).ToV2()) + require.EqualValues(t, 1000, eacl.MatchFromV2(1000)) }) } func TestFilterHeaderType(t *testing.T) { + require.Equal(t, eacl.HeaderTypeUnspecified, eacl.HeaderTypeUnknown) t.Run("known header types", func(t *testing.T) { - for i := eacl.HeaderTypeUnknown; i <= eacl.HeaderFromService; i++ { + for i := eacl.HeaderTypeUnspecified; i <= eacl.HeaderFromService; i++ { require.Equal(t, eqV2HeaderTypes[i], i.ToV2()) require.Equal(t, eacl.FilterHeaderTypeFromV2(i.ToV2()), i) } }) t.Run("unknown header types", func(t *testing.T) { - require.Equal(t, (eacl.HeaderFromService + 1).ToV2(), v2acl.HeaderTypeUnknown) - require.Equal(t, eacl.FilterHeaderTypeFromV2(v2acl.HeaderTypeService+1), eacl.HeaderTypeUnknown) + require.EqualValues(t, 1000, eacl.FilterHeaderType(1000).ToV2()) + require.EqualValues(t, 1000, eacl.FilterHeaderTypeFromV2(1000)) }) } @@ -152,7 +199,21 @@ func testEnumStrings(t *testing.T, e enumIface, items []enumStringItem) { } } +func TestActionProto(t *testing.T) { + for x, y := range map[v2acl.Action]eacl.Action{ + v2acl.ActionUnknown: eacl.ActionUnspecified, + v2acl.ActionAllow: eacl.ActionAllow, + v2acl.ActionDeny: eacl.ActionDeny, + } { + require.EqualValues(t, x, y) + } +} + func TestAction_String(t *testing.T) { + for a, s := range actionStrings { + require.Equal(t, s, a.String()) + } + toPtr := func(v eacl.Action) *eacl.Action { return &v } @@ -160,11 +221,56 @@ func TestAction_String(t *testing.T) { testEnumStrings(t, new(eacl.Action), []enumStringItem{ {val: toPtr(eacl.ActionAllow), str: "ALLOW"}, {val: toPtr(eacl.ActionDeny), str: "DENY"}, - {val: toPtr(eacl.ActionUnknown), str: "ACTION_UNSPECIFIED"}, + {val: toPtr(eacl.ActionUnspecified), str: "ACTION_UNSPECIFIED"}, }) } +type enum interface{ ~uint32 } + +func testEnumToString[T enum](t testing.TB, m map[T]string, f func(T) string) { + for n, s := range m { + require.Equal(t, s, f(n)) + } +} + +func testEnumFromString[T enum](t *testing.T, m map[T]string, f func(string) (T, bool)) { + t.Run("invalid", func(t *testing.T) { + for _, s := range []string{"", "foo", "1.2"} { + _, ok := f(s) + require.False(t, ok) + } + }) + for n, s := range m { + v, ok := f(s) + require.True(t, ok) + require.Equal(t, n, v) + } +} + +func TestActionToString(t *testing.T) { + testEnumToString(t, actionStrings, eacl.ActionToString) +} + +func TestActionFromString(t *testing.T) { + testEnumFromString(t, actionStrings, eacl.ActionFromString) +} + +func TestRoleProto(t *testing.T) { + for x, y := range map[v2acl.Role]eacl.Role{ + v2acl.RoleUnknown: eacl.RoleUnspecified, + v2acl.RoleUser: eacl.RoleUser, + v2acl.RoleSystem: eacl.RoleSystem, + v2acl.RoleOthers: eacl.RoleOthers, + } { + require.EqualValues(t, x, y) + } +} + func TestRole_String(t *testing.T) { + for r, s := range roleStrings { + require.Equal(t, s, r.String()) + } + toPtr := func(v eacl.Role) *eacl.Role { return &v } @@ -173,11 +279,38 @@ func TestRole_String(t *testing.T) { {val: toPtr(eacl.RoleUser), str: "USER"}, {val: toPtr(eacl.RoleSystem), str: "SYSTEM"}, {val: toPtr(eacl.RoleOthers), str: "OTHERS"}, - {val: toPtr(eacl.RoleUnknown), str: "ROLE_UNSPECIFIED"}, + {val: toPtr(eacl.RoleUnspecified), str: "ROLE_UNSPECIFIED"}, }) } +func TestRoleToString(t *testing.T) { + testEnumToString(t, roleStrings, eacl.RoleToString) +} + +func TestRoleFromString(t *testing.T) { + testEnumFromString(t, roleStrings, eacl.RoleFromString) +} + +func TestOperationProto(t *testing.T) { + for x, y := range map[v2acl.Operation]eacl.Operation{ + v2acl.OperationUnknown: eacl.OperationUnspecified, + v2acl.OperationGet: eacl.OperationGet, + v2acl.OperationHead: eacl.OperationHead, + v2acl.OperationPut: eacl.OperationPut, + v2acl.OperationDelete: eacl.OperationDelete, + v2acl.OperationSearch: eacl.OperationSearch, + v2acl.OperationRange: eacl.OperationRange, + v2acl.OperationRangeHash: eacl.OperationRangeHash, + } { + require.EqualValues(t, x, y) + } +} + func TestOperation_String(t *testing.T) { + for op, s := range opStrings { + require.Equal(t, s, op.String()) + } + toPtr := func(v eacl.Operation) *eacl.Operation { return &v } @@ -190,11 +323,38 @@ func TestOperation_String(t *testing.T) { {val: toPtr(eacl.OperationSearch), str: "SEARCH"}, {val: toPtr(eacl.OperationRange), str: "GETRANGE"}, {val: toPtr(eacl.OperationRangeHash), str: "GETRANGEHASH"}, - {val: toPtr(eacl.OperationUnknown), str: "OPERATION_UNSPECIFIED"}, + {val: toPtr(eacl.OperationUnspecified), str: "OPERATION_UNSPECIFIED"}, }) } +func TestOperationToString(t *testing.T) { + testEnumToString(t, opStrings, eacl.OperationToString) +} + +func TestOperationFromString(t *testing.T) { + testEnumFromString(t, opStrings, eacl.OperationFromString) +} + +func TestMatchProto(t *testing.T) { + for x, y := range map[v2acl.MatchType]eacl.Match{ + v2acl.MatchTypeUnknown: eacl.MatchUnspecified, + v2acl.MatchTypeStringEqual: eacl.MatchStringEqual, + v2acl.MatchTypeStringNotEqual: eacl.MatchStringNotEqual, + v2acl.MatchTypeNotPresent: eacl.MatchNotPresent, + v2acl.MatchTypeNumGT: eacl.MatchNumGT, + v2acl.MatchTypeNumGE: eacl.MatchNumGE, + v2acl.MatchTypeNumLT: eacl.MatchNumLT, + v2acl.MatchTypeNumLE: eacl.MatchNumLE, + } { + require.EqualValues(t, x, y) + } +} + func TestMatch_String(t *testing.T) { + for m, s := range matcherStrings { + require.Equal(t, s, m.String()) + } + toPtr := func(v eacl.Match) *eacl.Match { return &v } @@ -202,7 +362,7 @@ func TestMatch_String(t *testing.T) { testEnumStrings(t, new(eacl.Match), []enumStringItem{ {val: toPtr(eacl.MatchStringEqual), str: "STRING_EQUAL"}, {val: toPtr(eacl.MatchStringNotEqual), str: "STRING_NOT_EQUAL"}, - {val: toPtr(eacl.MatchUnknown), str: "MATCH_TYPE_UNSPECIFIED"}, + {val: toPtr(eacl.MatchUnspecified), str: "MATCH_TYPE_UNSPECIFIED"}, {val: toPtr(eacl.MatchNotPresent), str: "NOT_PRESENT"}, {val: toPtr(eacl.MatchNumGT), str: "NUM_GT"}, {val: toPtr(eacl.MatchNumGE), str: "NUM_GE"}, @@ -211,7 +371,30 @@ func TestMatch_String(t *testing.T) { }) } +func TestMatcherToString(t *testing.T) { + testEnumToString(t, matcherStrings, eacl.MatcherToString) +} + +func TestMatcherFromString(t *testing.T) { + testEnumFromString(t, matcherStrings, eacl.MatcherFromString) +} + +func TestFilterHeaderTypeProto(t *testing.T) { + for x, y := range map[v2acl.HeaderType]eacl.FilterHeaderType{ + v2acl.HeaderTypeUnknown: eacl.FilterHeaderType(0), + v2acl.HeaderTypeRequest: eacl.HeaderFromRequest, + v2acl.HeaderTypeObject: eacl.HeaderFromObject, + v2acl.HeaderTypeService: eacl.HeaderFromService, + } { + require.EqualValues(t, x, y) + } +} + func TestFilterHeaderType_String(t *testing.T) { + for h, s := range headerTypeStrings { + require.Equal(t, s, h.String()) + } + toPtr := func(v eacl.FilterHeaderType) *eacl.FilterHeaderType { return &v } @@ -219,6 +402,15 @@ func TestFilterHeaderType_String(t *testing.T) { testEnumStrings(t, new(eacl.FilterHeaderType), []enumStringItem{ {val: toPtr(eacl.HeaderFromRequest), str: "REQUEST"}, {val: toPtr(eacl.HeaderFromObject), str: "OBJECT"}, - {val: toPtr(eacl.HeaderTypeUnknown), str: "HEADER_UNSPECIFIED"}, + {val: toPtr(eacl.HeaderFromService), str: "SERVICE"}, + {val: toPtr(eacl.HeaderTypeUnspecified), str: "HEADER_UNSPECIFIED"}, }) } + +func TestHeaderTypeToString(t *testing.T) { + testEnumToString(t, headerTypeStrings, eacl.HeaderTypeToString) +} + +func TestHeaderTypeFromString(t *testing.T) { + testEnumFromString(t, headerTypeStrings, eacl.HeaderTypeFromString) +} diff --git a/eacl/filter.go b/eacl/filter.go index cfb2a2c7..8816b84a 100644 --- a/eacl/filter.go +++ b/eacl/filter.go @@ -4,12 +4,16 @@ import ( "strconv" v2acl "github.com/nspcc-dev/neofs-api-go/v2/acl" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + "github.com/nspcc-dev/neofs-sdk-go/user" ) -// Filter defines check conditions if request header is matched or not. Matched -// header means that request should be processed according to ContainerEACL action. +// Filter describes a binary property of an access-controlled NeoFS resource +// according to meta information about it. The meta information is represented +// by a set of key-value attributes of various types. // -// Filter is compatible with v2 acl.EACLRecord.Filter message. +// Filter should be created using one of the constructors. type Filter struct { from FilterHeaderType matcher Match @@ -17,31 +21,78 @@ type Filter struct { value stringEncoder } -type staticStringer string +func uint64Value(e uint64) string { return strconv.FormatUint(e, 10) } + +// ConstructFilter constructs new Filter instance. +func ConstructFilter(h FilterHeaderType, k string, m Match, v string) Filter { + return Filter{from: h, matcher: m, key: k, value: staticStringer(v)} +} + +// NewObjectPropertyFilter constructs new Filter for the object property. +func NewObjectPropertyFilter(k string, m Match, v string) Filter { + return ConstructFilter(HeaderFromObject, k, m, v) +} + +// NewFilterObjectWithID constructs Filter that limits the access rule to the +// referenced object only. +func NewFilterObjectWithID(obj oid.ID) Filter { + return NewObjectPropertyFilter(FilterObjectID, MatchStringEqual, obj.EncodeToString()) +} + +// NewFilterObjectsFromContainer constructs Filter that limits the access rule to +// objects from the referenced container only. +func NewFilterObjectsFromContainer(cnr cid.ID) Filter { + return NewObjectPropertyFilter(FilterObjectContainerID, MatchStringEqual, cnr.EncodeToString()) +} + +// NewFilterObjectOwnerEquals constructs Filter that limits the access rule to +// objects owner by the given user only. +func NewFilterObjectOwnerEquals(usr user.ID) Filter { + return NewObjectPropertyFilter(FilterObjectOwnerID, MatchStringEqual, usr.EncodeToString()) +} + +// NewFilterObjectCreationEpochIs constructs Filter that limits the access rule to +// objects with matching creation epoch only. +func NewFilterObjectCreationEpochIs(m Match, e uint64) Filter { + return NewObjectPropertyFilter(FilterObjectCreationEpoch, m, uint64Value(e)) +} + +// NewFilterObjectPayloadSizeIs constructs Filter that limits the access rule to +// objects with matching payload size only. +func NewFilterObjectPayloadSizeIs(m Match, e uint64) Filter { + return NewObjectPropertyFilter(FilterObjectPayloadSize, m, uint64Value(e)) +} + +// NewRequestHeaderFilter constructs new Filter for the request X-header. +func NewRequestHeaderFilter(k string, m Match, v string) Filter { + return ConstructFilter(HeaderFromRequest, k, m, v) +} + +// NewCustomServiceFilter constructs new Filter for the custom app-level +// property. +func NewCustomServiceFilter(k string, m Match, v string) Filter { + return ConstructFilter(HeaderFromService, k, m, v) +} -type u64Stringer uint64 +type staticStringer string // Various keys to object filters. const ( - FilterObjectVersion = v2acl.FilterObjectVersion - FilterObjectID = v2acl.FilterObjectID - FilterObjectContainerID = v2acl.FilterObjectContainerID - FilterObjectOwnerID = v2acl.FilterObjectOwnerID - FilterObjectCreationEpoch = v2acl.FilterObjectCreationEpoch - FilterObjectPayloadSize = v2acl.FilterObjectPayloadLength - FilterObjectPayloadChecksum = v2acl.FilterObjectPayloadHash - FilterObjectType = v2acl.FilterObjectType - FilterObjectPayloadHomomorphicChecksum = v2acl.FilterObjectHomomorphicHash + FilterObjectVersion = "$Object:version" + FilterObjectID = "$Object:objectID" + FilterObjectContainerID = "$Object:containerID" + FilterObjectOwnerID = "$Object:ownerID" + FilterObjectCreationEpoch = "$Object:creationEpoch" + FilterObjectPayloadSize = "$Object:payloadLength" + FilterObjectPayloadChecksum = "$Object:payloadHash" + FilterObjectType = "$Object:objectType" + FilterObjectPayloadHomomorphicChecksum = "$Object:homomorphicHash" ) func (s staticStringer) EncodeToString() string { return string(s) } -func (u u64Stringer) EncodeToString() string { - return strconv.FormatUint(uint64(u), 10) -} - // CopyTo writes deep copy of the [Filter] to dst. func (f Filter) CopyTo(dst *Filter) { dst.from = f.from @@ -50,22 +101,22 @@ func (f Filter) CopyTo(dst *Filter) { dst.value = f.value } -// Value returns filtered string value. +// Value returns value of the access-controlled resource's attribute to match. func (f Filter) Value() string { return f.value.EncodeToString() } -// Matcher returns filter Match type. +// Matcher returns operator to match the attribute. func (f Filter) Matcher() Match { return f.matcher } -// Key returns key to the filtered header. +// Key returns key to the access-controlled resource's attribute to match. func (f Filter) Key() string { return f.key } -// From returns FilterHeaderType that defined which header will be filtered. +// From returns type of access-controlled resource's attribute to match. func (f Filter) From() FilterHeaderType { return f.from } @@ -73,32 +124,47 @@ func (f Filter) From() FilterHeaderType { // ToV2 converts Filter to v2 acl.EACLRecord.Filter message. // // Nil Filter converts to nil. +// Deprecated: do not use it. func (f *Filter) ToV2() *v2acl.HeaderFilter { - if f == nil { - return nil + if f != nil { + return f.toProtoMessage() } + return nil +} +func (f Filter) toProtoMessage() *v2acl.HeaderFilter { filter := new(v2acl.HeaderFilter) filter.SetValue(f.value.EncodeToString()) filter.SetKey(f.key) - filter.SetMatchType(f.matcher.ToV2()) - filter.SetHeaderType(f.from.ToV2()) - + filter.SetMatchType(v2acl.MatchType(f.matcher)) + filter.SetHeaderType(v2acl.HeaderType(f.from)) return filter } +func (f *Filter) fromProtoMessage(m *v2acl.HeaderFilter) error { + f.from = FilterHeaderType(m.GetHeaderType()) + f.matcher = Match(m.GetMatchType()) + f.key = m.GetKey() + f.value = staticStringer(m.GetValue()) + return nil +} + // NewFilter creates, initializes and returns blank Filter instance. // // Defaults: -// - header type: HeaderTypeUnknown; -// - matcher: MatchUnknown; +// - header type: HeaderTypeUnspecified; +// - matcher: MatchUnspecified; // - key: ""; // - value: "". +// +// Deprecated: use [ConstructFilter] instead. func NewFilter() *Filter { - return NewFilterFromV2(new(v2acl.HeaderFilter)) + f := ConstructFilter(0, "", 0, "") + return &f } // NewFilterFromV2 converts v2 acl.EACLRecord.Filter message to Filter. +// Deprecated: do not use it. func NewFilterFromV2(filter *v2acl.HeaderFilter) *Filter { f := new(Filter) @@ -106,52 +172,34 @@ func NewFilterFromV2(filter *v2acl.HeaderFilter) *Filter { return f } - f.from = FilterHeaderTypeFromV2(filter.GetHeaderType()) - f.matcher = MatchFromV2(filter.GetMatchType()) - f.key = filter.GetKey() - f.value = staticStringer(filter.GetValue()) - + _ = f.fromProtoMessage(filter) return f } // Marshal marshals Filter into a protobuf binary form. -func (f *Filter) Marshal() []byte { - return f.ToV2().StableMarshal(nil) +func (f Filter) Marshal() []byte { + return f.toProtoMessage().StableMarshal(nil) } // Unmarshal unmarshals protobuf binary representation of Filter. func (f *Filter) Unmarshal(data []byte) error { - fV2 := new(v2acl.HeaderFilter) - if err := fV2.Unmarshal(data); err != nil { + m := new(v2acl.HeaderFilter) + if err := m.Unmarshal(data); err != nil { return err } - - *f = *NewFilterFromV2(fV2) - - return nil + return f.fromProtoMessage(m) } // MarshalJSON encodes Filter to protobuf JSON format. -func (f *Filter) MarshalJSON() ([]byte, error) { - return f.ToV2().MarshalJSON() +func (f Filter) MarshalJSON() ([]byte, error) { + return f.toProtoMessage().MarshalJSON() } // UnmarshalJSON decodes Filter from protobuf JSON format. func (f *Filter) UnmarshalJSON(data []byte) error { - fV2 := new(v2acl.HeaderFilter) - if err := fV2.UnmarshalJSON(data); err != nil { + m := new(v2acl.HeaderFilter) + if err := m.UnmarshalJSON(data); err != nil { return err } - - *f = *NewFilterFromV2(fV2) - - return nil -} - -// equalFilters compares Filter with each other. -func equalFilters(f1, f2 Filter) bool { - return f1.From() == f2.From() && - f1.Matcher() == f2.Matcher() && - f1.Key() == f2.Key() && - f1.Value() == f2.Value() + return f.fromProtoMessage(m) } diff --git a/eacl/filter_internal_test.go b/eacl/filter_internal_test.go new file mode 100644 index 00000000..a39c97a2 --- /dev/null +++ b/eacl/filter_internal_test.go @@ -0,0 +1,41 @@ +package eacl + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFilter_CopyTo(t *testing.T) { + var filter Filter + filter.value = staticStringer("value") + filter.from = 1 + filter.matcher = 1 + filter.key = "1" + + var dst Filter + t.Run("copy", func(t *testing.T) { + filter.CopyTo(&dst) + + require.Equal(t, filter, dst) + require.True(t, bytes.Equal(filter.Marshal(), dst.Marshal())) + }) + + t.Run("change", func(t *testing.T) { + require.Equal(t, filter.value, dst.value) + require.Equal(t, filter.from, dst.from) + require.Equal(t, filter.matcher, dst.matcher) + require.Equal(t, filter.key, dst.key) + + dst.value = staticStringer("value2") + dst.from = 2 + dst.matcher = 2 + dst.key = "2" + + require.NotEqual(t, filter.value, dst.value) + require.NotEqual(t, filter.from, dst.from) + require.NotEqual(t, filter.matcher, dst.matcher) + require.NotEqual(t, filter.key, dst.key) + }) +} diff --git a/eacl/filter_test.go b/eacl/filter_test.go index 6309453b..15e19db5 100644 --- a/eacl/filter_test.go +++ b/eacl/filter_test.go @@ -1,117 +1,210 @@ -package eacl +package eacl_test import ( - "bytes" + "encoding/json" + "math/rand" + "strconv" "testing" - "github.com/nspcc-dev/neofs-api-go/v2/acl" - v2acl "github.com/nspcc-dev/neofs-api-go/v2/acl" + protoacl "github.com/nspcc-dev/neofs-api-go/v2/acl" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + "github.com/nspcc-dev/neofs-sdk-go/eacl" + oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + "github.com/nspcc-dev/neofs-sdk-go/user" "github.com/stretchr/testify/require" ) -func newObjectFilter(match Match, key, val string) *Filter { - return &Filter{ - from: HeaderFromObject, - key: key, - matcher: match, - value: staticStringer(val), - } -} +func TestFilter_ToV2(t *testing.T) { + require.Nil(t, (*eacl.Filter)(nil).ToV2()) + key := "key_" + strconv.Itoa(rand.Int()) + val := "val_" + strconv.Itoa(rand.Int()) + r := eacl.ConstructFilter(anyValidHeaderType, key, anyValidMatcher, val) + m := r.ToV2() + require.EqualValues(t, anyValidHeaderType, m.GetHeaderType()) + require.Equal(t, key, m.GetKey()) + require.EqualValues(t, anyValidMatcher, m.GetMatchType()) + require.Equal(t, val, m.GetValue()) -func TestFilter(t *testing.T) { - filter := newObjectFilter(MatchStringEqual, "some name", "200") + t.Run("default values", func(t *testing.T) { + filter := eacl.NewFilter() - v2 := filter.ToV2() - require.NotNil(t, v2) - require.Equal(t, v2acl.HeaderTypeObject, v2.GetHeaderType()) - require.EqualValues(t, v2acl.MatchTypeStringEqual, v2.GetMatchType()) - require.Equal(t, filter.Key(), v2.GetKey()) - require.Equal(t, filter.Value(), v2.GetValue()) + // check initial values + require.Empty(t, filter.Key()) + require.Empty(t, filter.Value()) + require.Zero(t, filter.From()) + require.Zero(t, filter.Matcher()) - newFilter := NewFilterFromV2(v2) - require.Equal(t, filter, newFilter) + // convert to v2 message + filterV2 := filter.ToV2() - t.Run("from nil v2 filter", func(t *testing.T) { - require.Equal(t, new(Filter), NewFilterFromV2(nil)) + require.Empty(t, filterV2.GetKey()) + require.Empty(t, filterV2.GetValue()) + require.Zero(t, filterV2.GetHeaderType()) + require.Zero(t, filterV2.GetMatchType()) }) } -func TestFilterEncoding(t *testing.T) { - f := newObjectFilter(MatchStringEqual, "key", "value") - - t.Run("binary", func(t *testing.T) { - f2 := NewFilter() - require.NoError(t, f2.Unmarshal(f.Marshal())) +func TestNewFilterFromV2(t *testing.T) { + typ := protoacl.HeaderType(rand.Uint32()) + key := "key_" + strconv.Itoa(rand.Int()) + op := protoacl.MatchType(rand.Uint32()) + val := "val_" + strconv.Itoa(rand.Int()) + var m protoacl.HeaderFilter + m.SetHeaderType(typ) + m.SetKey(key) + m.SetMatchType(op) + m.SetValue(val) + + f := eacl.NewFilterFromV2(&m) + require.EqualValues(t, typ, f.From()) + require.Equal(t, key, f.Key()) + require.EqualValues(t, op, f.Matcher()) + require.Equal(t, val, f.Value()) - require.Equal(t, f, f2) + t.Run("nil", func(t *testing.T) { + require.Equal(t, new(eacl.Filter), eacl.NewFilterFromV2(nil)) }) +} - t.Run("json", func(t *testing.T) { - data, err := f.MarshalJSON() - require.NoError(t, err) +func TestFilter_Marshal(t *testing.T) { + for i := range anyValidFilters { + require.Equal(t, anyValidBinFilters[i], anyValidFilters[i].Marshal(), i) + } +} + +func TestFilter_Unmarshal(t *testing.T) { + t.Run("invalid protobuf", func(t *testing.T) { + err := new(eacl.Filter).Unmarshal([]byte("Hello, world!")) + require.ErrorContains(t, err, "proto") + require.ErrorContains(t, err, "cannot parse invalid wire-format data") + }) - d2 := NewFilter() - require.NoError(t, d2.UnmarshalJSON(data)) + var f eacl.Filter + for i := range anyValidBinFilters { + require.NoError(t, f.Unmarshal(anyValidBinFilters[i]), i) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/606") + require.EqualValues(t, anyValidFilters[i], f, i) + } +} - require.Equal(t, f, d2) +func TestFilter_MarshalJSON(t *testing.T) { + t.Run("invalid JSON", func(t *testing.T) { + err := new(eacl.Filter).UnmarshalJSON([]byte("Hello, world!")) + require.ErrorContains(t, err, "proto") + require.ErrorContains(t, err, "syntax error") }) + + var f1, f2 eacl.Filter + for i := range anyValidFilters { + b, err := anyValidFilters[i].MarshalJSON() + require.NoError(t, err, i) + require.NoError(t, f1.UnmarshalJSON(b), i) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/606") + require.Equal(t, anyValidFilters[i], f1, i) + + b, err = json.Marshal(anyValidFilters[i]) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(b, &f2), i) + require.Equal(t, anyValidFilters[i], f2, i) + } } -func TestFilter_ToV2(t *testing.T) { - t.Run("nil", func(t *testing.T) { - var x *Filter +func TestFilter_UnmarshalJSON(t *testing.T) { + var f1, f2 eacl.Filter + for i := range anyValidJSONFilters { + require.NoError(t, f1.UnmarshalJSON([]byte(anyValidJSONFilters[i])), i) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/606") + require.Equal(t, anyValidFilters[i], f1, i) - require.Nil(t, x.ToV2()) - }) + require.NoError(t, json.Unmarshal([]byte(anyValidJSONFilters[i]), &f2), i) + require.Equal(t, anyValidFilters[i], f2, i) + } +} - t.Run("default values", func(t *testing.T) { - filter := NewFilter() +func TestConstructFilter(t *testing.T) { + k := "Hello" + v := "World" + f := eacl.ConstructFilter(anyValidHeaderType, k, anyValidMatcher, v) + require.Equal(t, anyValidHeaderType, f.From()) + require.Equal(t, k, f.Key()) + require.Equal(t, anyValidMatcher, f.Matcher()) + require.Equal(t, v, f.Value()) +} - // check initial values - require.Empty(t, filter.Key()) - require.Empty(t, filter.Value()) - require.Equal(t, HeaderTypeUnknown, filter.From()) - require.Equal(t, MatchUnknown, filter.Matcher()) +func TestNewObjectPropertyFilter(t *testing.T) { + k := "Hello" + v := "World" + f := eacl.NewObjectPropertyFilter(k, anyValidMatcher, v) + require.Equal(t, eacl.HeaderFromObject, f.From()) + require.Equal(t, k, f.Key()) + require.Equal(t, anyValidMatcher, f.Matcher()) + require.Equal(t, v, f.Value()) +} - // convert to v2 message - filterV2 := filter.ToV2() +func TestNewRequestHeaderFilter(t *testing.T) { + k := "Hello" + v := "World" + f := eacl.NewRequestHeaderFilter(k, anyValidMatcher, v) + require.Equal(t, eacl.HeaderFromRequest, f.From()) + require.Equal(t, k, f.Key()) + require.Equal(t, anyValidMatcher, f.Matcher()) + require.Equal(t, v, f.Value()) +} - require.Empty(t, filterV2.GetKey()) - require.Empty(t, filterV2.GetValue()) - require.Equal(t, acl.HeaderTypeUnknown, filterV2.GetHeaderType()) - require.Equal(t, acl.MatchTypeUnknown, filterV2.GetMatchType()) - }) +func TestNewCustomServiceFilter(t *testing.T) { + k := "Hello" + v := "World" + f := eacl.NewCustomServiceFilter(k, anyValidMatcher, v) + require.Equal(t, eacl.HeaderFromService, f.From()) + require.Equal(t, k, f.Key()) + require.Equal(t, anyValidMatcher, f.Matcher()) + require.Equal(t, v, f.Value()) +} + +func TestFilterSingleObject(t *testing.T) { + obj := oid.ID{231, 189, 121, 7, 173, 134, 254, 165, 63, 186, 60, 89, 33, 95, 46, 103, + 217, 57, 164, 87, 82, 204, 251, 226, 1, 100, 32, 72, 251, 0, 7, 172} + f := eacl.NewFilterObjectWithID(obj) + require.Equal(t, eacl.HeaderFromObject, f.From()) + require.Equal(t, "$Object:objectID", f.Key()) + require.Equal(t, eacl.MatchStringEqual, f.Matcher()) + require.Equal(t, "GbckSBPEdM2P41Gkb9cVapFYb5HmRPDTZZp9JExGnsCF", f.Value()) } -func TestFilter_CopyTo(t *testing.T) { - var filter Filter - filter.value = staticStringer("value") - filter.from = 1 - filter.matcher = 1 - filter.key = "1" +func TestFilterObjectsFromContainer(t *testing.T) { + cnr := cid.ID{231, 189, 121, 7, 173, 134, 254, 165, 63, 186, 60, 89, 33, 95, 46, 103, + 217, 57, 164, 87, 82, 204, 251, 226, 1, 100, 32, 72, 251, 0, 7, 172} + f := eacl.NewFilterObjectsFromContainer(cnr) + require.Equal(t, eacl.HeaderFromObject, f.From()) + require.Equal(t, "$Object:containerID", f.Key()) + require.Equal(t, eacl.MatchStringEqual, f.Matcher()) + require.Equal(t, "GbckSBPEdM2P41Gkb9cVapFYb5HmRPDTZZp9JExGnsCF", f.Value()) +} - var dst Filter - t.Run("copy", func(t *testing.T) { - filter.CopyTo(&dst) +func TestFilterObjectOwnerEquals(t *testing.T) { + owner := user.ID{53, 51, 5, 166, 111, 29, 20, 101, 192, 165, 28, 167, 57, + 160, 82, 80, 41, 203, 20, 254, 30, 138, 195, 17, 92} + f := eacl.NewFilterObjectOwnerEquals(owner) + require.Equal(t, eacl.HeaderFromObject, f.From()) + require.Equal(t, "$Object:ownerID", f.Key()) + require.Equal(t, eacl.MatchStringEqual, f.Matcher()) + require.Equal(t, "NQZkR7mG74rJsGAHnpkiFeU9c4f5VLN54f", f.Value()) +} - require.Equal(t, filter, dst) - require.True(t, bytes.Equal(filter.Marshal(), dst.Marshal())) - }) +func TestFilterObjectCreationEpochIs(t *testing.T) { + const epoch = 657984300249 + f := eacl.NewFilterObjectCreationEpochIs(anyValidMatcher, epoch) + require.Equal(t, eacl.HeaderFromObject, f.From()) + require.Equal(t, "$Object:creationEpoch", f.Key()) + require.Equal(t, anyValidMatcher, f.Matcher()) + require.Equal(t, "657984300249", f.Value()) +} - t.Run("change", func(t *testing.T) { - require.Equal(t, filter.value, dst.value) - require.Equal(t, filter.from, dst.from) - require.Equal(t, filter.matcher, dst.matcher) - require.Equal(t, filter.key, dst.key) - - dst.value = staticStringer("value2") - dst.from = 2 - dst.matcher = 2 - dst.key = "2" - - require.NotEqual(t, filter.value, dst.value) - require.NotEqual(t, filter.from, dst.from) - require.NotEqual(t, filter.matcher, dst.matcher) - require.NotEqual(t, filter.key, dst.key) - }) +func TestFilterObjectPayloadSizeIs(t *testing.T) { + const sz = 4326750843582 + f := eacl.NewFilterObjectPayloadSizeIs(anyValidMatcher, sz) + require.Equal(t, eacl.HeaderFromObject, f.From()) + require.Equal(t, "$Object:payloadLength", f.Key()) + require.Equal(t, anyValidMatcher, f.Matcher()) + require.Equal(t, "4326750843582", f.Value()) } diff --git a/eacl/record.go b/eacl/record.go index 53d3a9ba..0a3ae0a4 100644 --- a/eacl/record.go +++ b/eacl/record.go @@ -2,6 +2,7 @@ package eacl import ( "crypto/ecdsa" + "fmt" v2acl "github.com/nspcc-dev/neofs-api-go/v2/acl" "github.com/nspcc-dev/neofs-sdk-go/checksum" @@ -12,10 +13,10 @@ import ( "github.com/nspcc-dev/neofs-sdk-go/version" ) -// Record of the ContainerEACL rule, that defines ContainerEACL action, targets for this action, -// object service operation and filters for request headers. +// Record represents an access rule operating in NeoFS access management. The +// rule is applied when some party requests access to a certain NeoFS resource. // -// Record is compatible with v2 acl.EACLRecord message. +// Record should be created using one of the constructors. type Record struct { action Action operation Operation @@ -23,6 +24,14 @@ type Record struct { targets []Target } +// ConstructRecord constructs new Record representing access rule regulating +// action in relation to specified target subjects when they perform the given +// operation. Optional filters allow to limit the effect of a rule on specific +// resources. +func ConstructRecord(a Action, op Operation, ts []Target, fs ...Filter) Record { + return Record{action: a, operation: op, filters: fs, targets: ts} +} + // CopyTo writes deep copy of the [Record] to dst. func (r Record) CopyTo(dst *Record) { dst.action = r.action @@ -40,7 +49,7 @@ func (r Record) CopyTo(dst *Record) { } } -// Targets returns list of target subjects to apply ACL rule to. +// Targets returns list of target subjects to which this access rule matches. // // The value returned shares memory with the structure itself, so changing it can lead to data corruption. // Make a copy if you need to change it. @@ -48,12 +57,14 @@ func (r Record) Targets() []Target { return r.targets } -// SetTargets sets list of target subjects to apply ACL rule to. +// SetTargets sets list of target subjects to which this access rule matches. func (r *Record) SetTargets(targets ...Target) { r.targets = targets } -// Filters returns list of filters to match and see if rule is applicable. +// Filters returns list of filters to match the requested resource to this +// access rule. Absence of filters means that Record is applicable to any +// resource. // // The value returned shares memory with the structure itself, so changing it can lead to data corruption. // Make a copy if you need to change it. @@ -61,33 +72,43 @@ func (r Record) Filters() []Filter { return r.filters } -// Operation returns NeoFS request verb to match. +// SetFilters returns list of filters to match the requested resource to this +// access rule. Empty list applies the Record to all resources. +func (r *Record) SetFilters(fs []Filter) { + r.filters = fs +} + +// Operation returns operation executed by the subject to match. func (r Record) Operation() Operation { return r.operation } -// SetOperation sets NeoFS request verb to match. +// SetOperation sets operation executed by the subject to match. func (r *Record) SetOperation(operation Operation) { r.operation = operation } -// Action returns rule execution result. +// Action returns action on the target subject when the access rule matches. func (r Record) Action() Action { return r.action } -// SetAction sets rule execution result. +// SetAction sets action on the target subject when the access rule matches. func (r *Record) SetAction(action Action) { r.action = action } // AddRecordTarget adds single Target to the Record. +// Deprecated: use [Record.SetTargets] instead. func AddRecordTarget(r *Record, t *Target) { r.SetTargets(append(r.Targets(), *t)...) } // AddFormedTarget forms Target with specified Role and list of // ECDSA public keys and adds it to the Record. +// Deprecated: use [Record.SetTargets] with [TargetByRole] or +// [TargetByPublicKeys] instead. Note that role and public keys are mutually +// exclusive. func AddFormedTarget(r *Record, role Role, keys ...ecdsa.PublicKey) { t := NewTarget() t.SetRole(role) @@ -100,114 +121,117 @@ type stringEncoder interface { EncodeToString() string } -func (r *Record) addFilter(from FilterHeaderType, m Match, key string, val stringEncoder) { - filter := Filter{ - from: from, - key: key, - matcher: m, - value: val, - } - - r.filters = append(r.filters, filter) -} - -func (r *Record) addObjectFilter(m Match, key string, val stringEncoder) { - r.addFilter(HeaderFromObject, m, key, val) -} - // AddFilter adds generic filter. // // If matcher is [MatchNotPresent], the value must be empty. If matcher is // numeric (e.g. [MatchNumGT]), value must be a base-10 integer. +// Deprecated: use [ConstructRecord] with [ConstructFilter] instead. func (r *Record) AddFilter(from FilterHeaderType, matcher Match, name, value string) { - r.addFilter(from, matcher, name, staticStringer(value)) + r.SetFilters(append(r.Filters(), ConstructFilter(from, name, matcher, value))) } // AddObjectAttributeFilter adds filter by object attribute. // // If m is [MatchNotPresent], the value must be empty. If matcher is numeric // (e.g. [MatchNumGT]), value must be a base-10 integer. +// Deprecated: use [ConstructRecord] with [NewObjectPropertyFilter] instead. func (r *Record) AddObjectAttributeFilter(m Match, key, value string) { - r.addObjectFilter(m, key, staticStringer(value)) + r.SetFilters(append(r.Filters(), NewObjectPropertyFilter(key, m, value))) } // AddObjectVersionFilter adds filter by object version. // // The m must not be [MatchNotPresent] or numeric (e.g. [MatchNumGT]). +// Deprecated: use [ConstructRecord] with [NewObjectPropertyFilter] instead. func (r *Record) AddObjectVersionFilter(m Match, v *version.Version) { - r.addObjectFilter(m, FilterObjectVersion, staticStringer(version.EncodeToString(*v))) + r.SetFilters(append(r.Filters(), NewObjectPropertyFilter(FilterObjectVersion, m, version.EncodeToString(*v)))) } // AddObjectIDFilter adds filter by object ID. // // The m must not be [MatchNotPresent] or numeric (e.g. [MatchNumGT]). +// Deprecated: use [ConstructRecord] with [NewObjectPropertyFilter] or +// [NewFilterObjectWithID] instead. func (r *Record) AddObjectIDFilter(m Match, id oid.ID) { - r.addObjectFilter(m, FilterObjectID, id) + r.SetFilters(append(r.Filters(), NewObjectPropertyFilter(FilterObjectID, m, id.EncodeToString()))) } // AddObjectContainerIDFilter adds filter by object container ID. // // The m must not be [MatchNotPresent] or numeric (e.g. [MatchNumGT]). +// Deprecated: use [ConstructRecord] with [NewObjectPropertyFilter] or +// [NewFilterObjectsFromContainer] instead. func (r *Record) AddObjectContainerIDFilter(m Match, id cid.ID) { - r.addObjectFilter(m, FilterObjectContainerID, id) + r.SetFilters(append(r.Filters(), NewObjectPropertyFilter(FilterObjectContainerID, m, id.EncodeToString()))) } // AddObjectOwnerIDFilter adds filter by object owner ID. // // The m must not be [MatchNotPresent] or numeric (e.g. [MatchNumGT]). +// Deprecated: use [ConstructRecord] with [NewObjectPropertyFilter] or +// [NewFilterObjectOwnerEquals] instead. func (r *Record) AddObjectOwnerIDFilter(m Match, id *user.ID) { - r.addObjectFilter(m, FilterObjectOwnerID, id) + r.SetFilters(append(r.Filters(), NewObjectPropertyFilter(FilterObjectOwnerID, m, id.EncodeToString()))) } // AddObjectCreationEpoch adds filter by object creation epoch. // // The m must not be [MatchNotPresent]. +// Deprecated: use [ConstructRecord] with [NewFilterObjectCreationEpochIs] instead. func (r *Record) AddObjectCreationEpoch(m Match, epoch uint64) { - r.addObjectFilter(m, FilterObjectCreationEpoch, u64Stringer(epoch)) + r.SetFilters(append(r.Filters(), NewFilterObjectCreationEpochIs(m, epoch))) } // AddObjectPayloadLengthFilter adds filter by object payload length. // // The m must not be [MatchNotPresent]. +// Deprecated: use [ConstructRecord] with [NewFilterObjectPayloadSizeIs] instead. func (r *Record) AddObjectPayloadLengthFilter(m Match, size uint64) { - r.addObjectFilter(m, FilterObjectPayloadSize, u64Stringer(size)) + r.SetFilters(append(r.Filters(), NewFilterObjectPayloadSizeIs(m, size))) } // AddObjectPayloadHashFilter adds filter by object payload hash value. // // The m must not be [MatchNotPresent] or numeric (e.g. [MatchNumGT]). +// Deprecated: use [ConstructRecord] with [NewObjectPropertyFilter] instead. func (r *Record) AddObjectPayloadHashFilter(m Match, h checksum.Checksum) { - r.addObjectFilter(m, FilterObjectPayloadChecksum, staticStringer(h.String())) + r.SetFilters(append(r.Filters(), NewObjectPropertyFilter(FilterObjectPayloadChecksum, m, h.String()))) } // AddObjectTypeFilter adds filter by object type. // // The m must not be [MatchNotPresent] or numeric (e.g. [MatchNumGT]). +// Deprecated: use [ConstructRecord] with [NewObjectPropertyFilter] instead. func (r *Record) AddObjectTypeFilter(m Match, t object.Type) { - r.addObjectFilter(m, FilterObjectType, staticStringer(t.EncodeToString())) + r.SetFilters(append(r.Filters(), NewObjectPropertyFilter(FilterObjectType, m, t.EncodeToString()))) } // AddObjectHomomorphicHashFilter adds filter by object payload homomorphic hash value. // // The m must not be [MatchNotPresent] or numeric (e.g. [MatchNumGT]). +// Deprecated: use [ConstructRecord] with [NewObjectPropertyFilter] instead. func (r *Record) AddObjectHomomorphicHashFilter(m Match, h checksum.Checksum) { - r.addObjectFilter(m, FilterObjectPayloadHomomorphicChecksum, staticStringer(h.String())) + r.SetFilters(append(r.Filters(), NewObjectPropertyFilter(FilterObjectPayloadHomomorphicChecksum, m, h.String()))) } // ToV2 converts Record to v2 acl.EACLRecord message. // // Nil Record converts to nil. +// Deprecated: do not use it. func (r *Record) ToV2() *v2acl.Record { - if r == nil { - return nil + if r != nil { + return r.toProtoMessage() } + return nil +} +func (r Record) toProtoMessage() *v2acl.Record { v2 := new(v2acl.Record) if r.targets != nil { targets := make([]v2acl.Target, len(r.targets)) for i := range r.targets { - targets[i] = *r.targets[i].ToV2() + targets[i] = *r.targets[i].toProtoMessage() } v2.SetTargets(targets) @@ -216,30 +240,56 @@ func (r *Record) ToV2() *v2acl.Record { if r.filters != nil { filters := make([]v2acl.HeaderFilter, len(r.filters)) for i := range r.filters { - filters[i] = *r.filters[i].ToV2() + filters[i] = *r.filters[i].toProtoMessage() } v2.SetFilters(filters) } - v2.SetAction(r.action.ToV2()) - v2.SetOperation(r.operation.ToV2()) + v2.SetAction(v2acl.Action(r.action)) + v2.SetOperation(v2acl.Operation(r.operation)) return v2 } +func (r *Record) fromProtoMessage(m *v2acl.Record) error { + mt := m.GetTargets() + r.targets = make([]Target, len(mt)) + for i := range mt { + if err := r.targets[i].fromProtoMessage(&mt[i]); err != nil { + return fmt.Errorf("invalid subject descriptor #%d: %w", i, err) + } + } + + mf := m.GetFilters() + r.filters = make([]Filter, len(mf)) + for i := range mf { + if err := r.filters[i].fromProtoMessage(&mf[i]); err != nil { + return fmt.Errorf("invalid filter #%d: %w", i, err) + } + } + + r.action = Action(m.GetAction()) + r.operation = Operation(m.GetOperation()) + + return nil +} + // NewRecord creates and returns blank Record instance. // // Defaults: -// - action: ActionUnknown; -// - operation: OperationUnknown; +// - action: ActionUnspecified; +// - operation: OperationUnspecified; // - targets: nil, // - filters: nil. +// +// Deprecated: use [ConstructRecord] instead. func NewRecord() *Record { return new(Record) } // CreateRecord creates, initializes with parameters and returns Record instance. +// Deprecated: use [ConstructRecord] instead. func CreateRecord(action Action, operation Operation) *Record { r := NewRecord() r.action = action @@ -251,6 +301,7 @@ func CreateRecord(action Action, operation Operation) *Record { } // NewRecordFromV2 converts v2 acl.EACLRecord message to Record. +// Deprecated: do not use it. func NewRecordFromV2(record *v2acl.Record) *Record { r := NewRecord() @@ -258,85 +309,34 @@ func NewRecordFromV2(record *v2acl.Record) *Record { return r } - r.action = ActionFromV2(record.GetAction()) - r.operation = OperationFromV2(record.GetOperation()) - - v2targets := record.GetTargets() - v2filters := record.GetFilters() - - r.targets = make([]Target, len(v2targets)) - for i := range v2targets { - r.targets[i] = *NewTargetFromV2(&v2targets[i]) - } - - r.filters = make([]Filter, len(v2filters)) - for i := range v2filters { - r.filters[i] = *NewFilterFromV2(&v2filters[i]) - } - + _ = r.fromProtoMessage(record) return r } // Marshal marshals Record into a protobuf binary form. -func (r *Record) Marshal() []byte { - return r.ToV2().StableMarshal(nil) +func (r Record) Marshal() []byte { + return r.toProtoMessage().StableMarshal(nil) } // Unmarshal unmarshals protobuf binary representation of Record. func (r *Record) Unmarshal(data []byte) error { - fV2 := new(v2acl.Record) - if err := fV2.Unmarshal(data); err != nil { + m := new(v2acl.Record) + if err := m.Unmarshal(data); err != nil { return err } - - *r = *NewRecordFromV2(fV2) - - return nil + return r.fromProtoMessage(m) } // MarshalJSON encodes Record to protobuf JSON format. -func (r *Record) MarshalJSON() ([]byte, error) { - return r.ToV2().MarshalJSON() +func (r Record) MarshalJSON() ([]byte, error) { + return r.toProtoMessage().MarshalJSON() } // UnmarshalJSON decodes Record from protobuf JSON format. func (r *Record) UnmarshalJSON(data []byte) error { - tV2 := new(v2acl.Record) - if err := tV2.UnmarshalJSON(data); err != nil { + m := new(v2acl.Record) + if err := m.UnmarshalJSON(data); err != nil { return err } - - *r = *NewRecordFromV2(tV2) - - return nil -} - -// equalRecords compares Record with each other. -func equalRecords(r1, r2 Record) bool { - if r1.Operation() != r2.Operation() || - r1.Action() != r2.Action() { - return false - } - - fs1, fs2 := r1.Filters(), r2.Filters() - ts1, ts2 := r1.Targets(), r2.Targets() - - if len(fs1) != len(fs2) || - len(ts1) != len(ts2) { - return false - } - - for i := 0; i < len(fs1); i++ { - if !equalFilters(fs1[i], fs2[i]) { - return false - } - } - - for i := 0; i < len(ts1); i++ { - if !equalTargets(ts1[i], ts2[i]) { - return false - } - } - - return true + return r.fromProtoMessage(m) } diff --git a/eacl/record_internal_test.go b/eacl/record_internal_test.go new file mode 100644 index 00000000..b635ec4e --- /dev/null +++ b/eacl/record_internal_test.go @@ -0,0 +1,67 @@ +package eacl + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRecord_CopyTo(t *testing.T) { + var record Record + record.action = ActionAllow + record.operation = OperationPut + record.AddObjectAttributeFilter(MatchStringEqual, "key", "value") + + var target Target + target.SetRole(1) + target.SetBinaryKeys([][]byte{ + {1, 2, 3}, + }) + + record.SetTargets(target) + record.AddObjectAttributeFilter(MatchStringEqual, "key", "value") + + t.Run("copy", func(t *testing.T) { + var dst Record + record.CopyTo(&dst) + + require.Equal(t, record, dst) + require.True(t, bytes.Equal(record.Marshal(), dst.Marshal())) + }) + + t.Run("change filters", func(t *testing.T) { + var dst Record + record.CopyTo(&dst) + + require.Equal(t, record.filters[0].key, dst.filters[0].key) + require.Equal(t, record.filters[0].matcher, dst.filters[0].matcher) + require.Equal(t, record.filters[0].value, dst.filters[0].value) + require.Equal(t, record.filters[0].from, dst.filters[0].from) + + dst.filters[0].key = "key2" + dst.filters[0].matcher = MatchStringNotEqual + dst.filters[0].value = staticStringer("staticStringer") + dst.filters[0].from = 12345 + + require.NotEqual(t, record.filters[0].key, dst.filters[0].key) + require.NotEqual(t, record.filters[0].matcher, dst.filters[0].matcher) + require.NotEqual(t, record.filters[0].value, dst.filters[0].value) + require.NotEqual(t, record.filters[0].from, dst.filters[0].from) + }) + + t.Run("change target", func(t *testing.T) { + var dst Record + record.CopyTo(&dst) + + require.Equal(t, record.targets[0].role, dst.targets[0].role) + dst.targets[0].role = 12345 + require.NotEqual(t, record.targets[0].role, dst.targets[0].role) + + for i, key := range dst.targets[0].subjs { + require.True(t, bytes.Equal(key, record.targets[0].subjs[i])) + key[0] = 10 + require.False(t, bytes.Equal(key, record.targets[0].subjs[i])) + } + }) +} diff --git a/eacl/record_test.go b/eacl/record_test.go index eb1067ef..920b81f2 100644 --- a/eacl/record_test.go +++ b/eacl/record_test.go @@ -1,310 +1,272 @@ -package eacl +package eacl_test import ( - "bytes" - "crypto/ecdsa" - "fmt" + "encoding/json" + "math/rand" + "strconv" "testing" - "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - v2acl "github.com/nspcc-dev/neofs-api-go/v2/acl" - checksumtest "github.com/nspcc-dev/neofs-sdk-go/checksum/test" - cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" - "github.com/nspcc-dev/neofs-sdk-go/object" - oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test" - usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" - versiontest "github.com/nspcc-dev/neofs-sdk-go/version/test" + protoacl "github.com/nspcc-dev/neofs-api-go/v2/acl" + "github.com/nspcc-dev/neofs-sdk-go/eacl" "github.com/stretchr/testify/require" ) -func TestRecord(t *testing.T) { - record := NewRecord() - record.SetOperation(OperationRange) - record.SetAction(ActionAllow) - record.AddFilter(HeaderFromRequest, MatchStringEqual, "A", "B") - record.AddFilter(HeaderFromRequest, MatchStringNotEqual, "C", "D") - - target := NewTarget() - target.SetRole(RoleSystem) - AddRecordTarget(record, target) - - v2 := record.ToV2() - require.NotNil(t, v2) - require.Equal(t, v2acl.OperationRange, v2.GetOperation()) - require.Equal(t, v2acl.ActionAllow, v2.GetAction()) - require.Len(t, v2.GetFilters(), len(record.Filters())) - require.Len(t, v2.GetTargets(), len(record.Targets())) - - newRecord := NewRecordFromV2(v2) - require.Equal(t, record, newRecord) - - t.Run("create record", func(t *testing.T) { - record := CreateRecord(ActionAllow, OperationGet) - require.Equal(t, ActionAllow, record.Action()) - require.Equal(t, OperationGet, record.Operation()) - }) - - t.Run("new from nil v2 record", func(t *testing.T) { - require.Equal(t, new(Record), NewRecordFromV2(nil)) - }) +func TestAddFormedTarget(t *testing.T) { + var r eacl.Record + require.Zero(t, r.Targets()) + + eacl.AddFormedTarget(&r, eacl.RoleUnspecified, anyECDSAPublicKeys...) + require.Len(t, r.Targets(), 1) + require.Zero(t, r.Targets()[0].Role()) + require.Equal(t, anyValidECDSABinPublicKeys, r.Targets()[0].BinaryKeys()) + + role := eacl.Role(rand.Uint32()) + eacl.AddFormedTarget(&r, role) + require.Len(t, r.Targets(), 2) + require.Equal(t, role, r.Targets()[1].Role()) + require.Zero(t, r.Targets()[1].BinaryKeys()) } -func TestAddFormedTarget(t *testing.T) { - items := []struct { - role Role - keys []ecdsa.PublicKey - }{ - { - role: RoleUnknown, - keys: []ecdsa.PublicKey{*randomPublicKey(t)}, - }, - { - role: RoleSystem, - keys: []ecdsa.PublicKey{}, - }, +func TestRecord_AddFilter(t *testing.T) { + r := eacl.NewRecord() + for _, filter := range anyValidFilters { + r.AddFilter(filter.From(), filter.Matcher(), filter.Key(), filter.Value()) } - targets := make([]Target, len(items)) + require.Equal(t, anyValidFilters, r.Filters()) +} - r := NewRecord() +func TestRecord_ToV2(t *testing.T) { + require.Nil(t, (*eacl.Record)(nil).ToV2()) + r := eacl.ConstructRecord(anyValidAction, anyValidOp, anyValidTargets, anyValidFilters...) + m := r.ToV2() + require.EqualValues(t, anyValidAction, m.GetAction()) + require.EqualValues(t, anyValidOp, m.GetOperation()) + assertProtoTargetsEqual(t, anyValidTargets, m.GetTargets()) + assertProtoFiltersEqual(t, anyValidFilters, m.GetFilters()) - for i := range items { - targets[i].SetRole(items[i].role) - SetTargetECDSAKeys(&targets[i], ecdsaKeysToPtrs(items[i].keys)...) - AddFormedTarget(r, items[i].role, items[i].keys...) - } + t.Run("default values", func(t *testing.T) { + record := eacl.NewRecord() - tgts := r.Targets() - require.Len(t, tgts, len(targets)) + // check initial values + require.Zero(t, record.Operation()) + require.Zero(t, record.Action()) + require.Nil(t, record.Targets()) + require.Nil(t, record.Filters()) - for _, tgt := range targets { - require.Contains(t, tgts, tgt) - } + // convert to v2 message + recordV2 := record.ToV2() + + require.Zero(t, recordV2.GetOperation()) + require.Zero(t, recordV2.GetAction()) + require.Nil(t, recordV2.GetTargets()) + require.Nil(t, recordV2.GetFilters()) + }) } -func TestRecord_AddFilter(t *testing.T) { - filters := []Filter{ - *newObjectFilter(MatchStringEqual, "some name", "ContainerID"), - *newObjectFilter(MatchStringNotEqual, "X-Header-Name", "X-Header-Value"), +func TestNewRecordFromV2(t *testing.T) { + a := protoacl.Action(rand.Uint32()) + op := protoacl.Operation(rand.Uint32()) + ts := make([]protoacl.Target, 2) + for i := range ts { + ts[i].SetRole(protoacl.Role(rand.Uint32())) + ts[i].SetKeys(anyValidBinPublicKeys) } - - r := NewRecord() - for _, filter := range filters { - r.AddFilter(filter.From(), filter.Matcher(), filter.Key(), filter.Value()) + fs := make([]protoacl.HeaderFilter, 2) + for i := range fs { + fs[i].SetHeaderType(protoacl.HeaderType(rand.Uint32())) + fs[i].SetKey("key_" + strconv.Itoa(rand.Int())) + fs[i].SetMatchType(protoacl.MatchType(rand.Uint32())) + fs[i].SetValue("val_" + strconv.Itoa(rand.Int())) } + var m protoacl.Record + m.SetAction(a) + m.SetOperation(op) + m.SetTargets(ts) + m.SetFilters(fs) + + r := eacl.NewRecordFromV2(&m) + require.EqualValues(t, a, r.Action()) + require.EqualValues(t, op, r.Operation()) + assertProtoTargetsEqual(t, r.Targets(), ts) + assertProtoFiltersEqual(t, r.Filters(), fs) - require.Equal(t, filters, r.Filters()) + t.Run("nil", func(t *testing.T) { + require.Equal(t, new(eacl.Record), eacl.NewRecordFromV2(nil)) + }) } -func TestRecordEncoding(t *testing.T) { - r := NewRecord() - r.SetOperation(OperationHead) - r.SetAction(ActionDeny) - r.AddObjectAttributeFilter(MatchStringEqual, "key", "value") - AddFormedTarget(r, RoleSystem, *randomPublicKey(t)) - - t.Run("binary", func(t *testing.T) { - r2 := NewRecord() - require.NoError(t, r2.Unmarshal(r.Marshal())) +func TestRecord_Marshal(t *testing.T) { + for i := range anyValidRecords { + require.Equal(t, anyValidBinRecords[i], anyValidRecords[i].Marshal(), i) + } +} - require.Equal(t, r, r2) +func TestRecord_Unmarshal(t *testing.T) { + t.Run("invalid protobuf", func(t *testing.T) { + err := new(eacl.Record).Unmarshal([]byte("Hello, world!")) + require.ErrorContains(t, err, "proto") + require.ErrorContains(t, err, "cannot parse invalid wire-format data") }) - t.Run("json", func(t *testing.T) { - data, err := r.MarshalJSON() - require.NoError(t, err) - - r2 := NewRecord() - require.NoError(t, r2.UnmarshalJSON(data)) + var r eacl.Record + for i := range anyValidBinRecords { + require.NoError(t, r.Unmarshal(anyValidBinRecords[i]), i) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/606") + require.EqualValues(t, anyValidRecords[i], r, i) + } +} - require.Equal(t, r, r2) +func TestRecord_MarshalJSON(t *testing.T) { + t.Run("invalid JSON", func(t *testing.T) { + err := new(eacl.Record).UnmarshalJSON([]byte("Hello, world!")) + require.ErrorContains(t, err, "proto") + require.ErrorContains(t, err, "syntax error") }) -} -func TestRecord_ToV2(t *testing.T) { - t.Run("nil", func(t *testing.T) { - var x *Record + var r1, r2 eacl.Record + for i := range anyValidRecords { + b, err := anyValidRecords[i].MarshalJSON() + require.NoError(t, err, i) + require.NoError(t, r1.UnmarshalJSON(b), i) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/606") + require.Equal(t, anyValidRecords[i], r1, i) - require.Nil(t, x.ToV2()) - }) + b, err = json.Marshal(anyValidRecords[i]) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(b, &r2), i) + require.Equal(t, anyValidRecords[i], r2, i) + } +} - t.Run("default values", func(t *testing.T) { - record := NewRecord() +func TestRecord_UnmarshalJSON(t *testing.T) { + var r1, r2 eacl.Record + for i := range anyValidJSONRecords { + require.NoError(t, r1.UnmarshalJSON([]byte(anyValidJSONRecords[i])), i) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/606") + require.Equal(t, anyValidFilters[i], r1, i) - // check initial values - require.Equal(t, OperationUnknown, record.Operation()) - require.Equal(t, ActionUnknown, record.Action()) - require.Nil(t, record.Targets()) - require.Nil(t, record.Filters()) + require.NoError(t, json.Unmarshal([]byte(anyValidJSONRecords[i]), &r2), i) + require.Equal(t, anyValidJSONRecords[i], r2, i) + } +} - // convert to v2 message - recordV2 := record.ToV2() +func assertSingleObjectFilter(t testing.TB, r eacl.Record, k string, m eacl.Match, v string) { + require.Len(t, r.Filters(), 1) + require.EqualValues(t, 2, r.Filters()[0].From()) + require.Equal(t, k, r.Filters()[0].Key()) + require.EqualValues(t, m, r.Filters()[0].Matcher()) + require.Equal(t, v, r.Filters()[0].Value()) +} - require.Equal(t, v2acl.OperationUnknown, recordV2.GetOperation()) - require.Equal(t, v2acl.ActionUnknown, recordV2.GetAction()) - require.Nil(t, recordV2.GetTargets()) - require.Nil(t, recordV2.GetFilters()) - }) +func TestRecord_AddObjectAttributeFilter(t *testing.T) { + var r eacl.Record + r.AddObjectAttributeFilter(anyValidMatcher, "foo", "bar") + assertSingleObjectFilter(t, r, "foo", anyValidMatcher, "bar") } -func TestReservedRecords(t *testing.T) { - var ( - v = versiontest.Version() - oid = oidtest.ID() - cid = cidtest.ID() - ownerid = usertest.ID() - h = checksumtest.Checksum() - typ = new(object.Type) - ) - - testSuit := []struct { - f func(r *Record) - key string - value string - }{ - { - f: func(r *Record) { r.AddObjectAttributeFilter(MatchStringEqual, "foo", "bar") }, - key: "foo", - value: "bar", - }, - { - f: func(r *Record) { r.AddObjectVersionFilter(MatchStringEqual, &v) }, - key: v2acl.FilterObjectVersion, - value: v.String(), - }, - { - f: func(r *Record) { r.AddObjectIDFilter(MatchStringEqual, oid) }, - key: v2acl.FilterObjectID, - value: oid.EncodeToString(), - }, - { - f: func(r *Record) { r.AddObjectContainerIDFilter(MatchStringEqual, cid) }, - key: v2acl.FilterObjectContainerID, - value: cid.EncodeToString(), - }, - { - f: func(r *Record) { r.AddObjectOwnerIDFilter(MatchStringEqual, &ownerid) }, - key: v2acl.FilterObjectOwnerID, - value: ownerid.EncodeToString(), - }, - { - f: func(r *Record) { r.AddObjectCreationEpoch(MatchStringEqual, 100) }, - key: v2acl.FilterObjectCreationEpoch, - value: "100", - }, - { - f: func(r *Record) { r.AddObjectPayloadLengthFilter(MatchStringEqual, 5000) }, - key: v2acl.FilterObjectPayloadLength, - value: "5000", - }, - { - f: func(r *Record) { r.AddObjectPayloadHashFilter(MatchStringEqual, h) }, - key: v2acl.FilterObjectPayloadHash, - value: h.String(), - }, - { - f: func(r *Record) { r.AddObjectHomomorphicHashFilter(MatchStringEqual, h) }, - key: v2acl.FilterObjectHomomorphicHash, - value: h.String(), - }, - { - f: func(r *Record) { - require.True(t, typ.DecodeString("REGULAR")) - r.AddObjectTypeFilter(MatchStringEqual, *typ) - }, - key: v2acl.FilterObjectType, - value: "REGULAR", - }, - { - f: func(r *Record) { - require.True(t, typ.DecodeString("TOMBSTONE")) - r.AddObjectTypeFilter(MatchStringEqual, *typ) - }, - key: v2acl.FilterObjectType, - value: "TOMBSTONE", - }, - { - f: func(r *Record) { - require.True(t, typ.DecodeString("STORAGE_GROUP")) - r.AddObjectTypeFilter(MatchStringEqual, *typ) - }, - key: v2acl.FilterObjectType, - value: "STORAGE_GROUP", - }, - } +func TestRecord_AddObjectIDFilter(t *testing.T) { + var r eacl.Record + r.AddObjectIDFilter(anyValidMatcher, anyValidObjectID) + assertSingleObjectFilter(t, r, "$Object:objectID", anyValidMatcher, anyValidObjectIDString) +} - for n, testCase := range testSuit { - desc := fmt.Sprintf("case #%d", n) - record := NewRecord() - testCase.f(record) - require.Len(t, record.Filters(), 1, desc) - f := record.Filters()[0] - require.Equal(t, f.Key(), testCase.key, desc) - require.Equal(t, f.Value(), testCase.value, desc) - } +func TestRecord_AddObjectVersionFilter(t *testing.T) { + var r eacl.Record + r.AddObjectVersionFilter(anyValidMatcher, &anyValidProtoVersion) + assertSingleObjectFilter(t, r, "$Object:version", anyValidMatcher, anyValidProtoVersionString) } -func randomPublicKey(t *testing.T) *ecdsa.PublicKey { - p, err := keys.NewPrivateKey() - require.NoError(t, err) - return &p.PrivateKey.PublicKey +func TestRecord_AddObjectContainerIDFilter(t *testing.T) { + var r eacl.Record + r.AddObjectContainerIDFilter(anyValidMatcher, anyValidContainerID) + assertSingleObjectFilter(t, r, "$Object:containerID", anyValidMatcher, anyValidContainerIDString) } -func TestRecord_CopyTo(t *testing.T) { - var record Record - record.action = ActionAllow - record.operation = OperationPut - record.AddObjectAttributeFilter(MatchStringEqual, "key", "value") +func TestRecord_AddObjectOwnerIDFilter(t *testing.T) { + var r eacl.Record + r.AddObjectOwnerIDFilter(anyValidMatcher, &anyValidUserID) + assertSingleObjectFilter(t, r, "$Object:ownerID", anyValidMatcher, anyValidUserIDString) +} - var target Target - target.SetRole(1) - target.SetBinaryKeys([][]byte{ - {1, 2, 3}, - }) +func TestRecord_AddObjectCreationEpoch(t *testing.T) { + var r eacl.Record + r.AddObjectCreationEpoch(anyValidMatcher, 673984) + assertSingleObjectFilter(t, r, "$Object:creationEpoch", anyValidMatcher, "673984") +} - record.SetTargets(target) - record.AddObjectAttributeFilter(MatchStringEqual, "key", "value") +func TestRecord_AddObjectPayloadLengthFilter(t *testing.T) { + var r eacl.Record + r.AddObjectPayloadLengthFilter(anyValidMatcher, 74928) + assertSingleObjectFilter(t, r, "$Object:payloadLength", anyValidMatcher, "74928") +} - t.Run("copy", func(t *testing.T) { - var dst Record - record.CopyTo(&dst) +func TestRecord_AddObjectPayloadHashFilter(t *testing.T) { + for i := range anyValidChecksums { + var r eacl.Record + r.AddObjectPayloadHashFilter(anyValidMatcher, anyValidChecksums[i]) + assertSingleObjectFilter(t, r, "$Object:payloadHash", anyValidMatcher, anyValidStringChecksums[i]) + } +} - require.Equal(t, record, dst) - require.True(t, bytes.Equal(record.Marshal(), dst.Marshal())) - }) +func TestRecord_AddObjectHomomorphicHashFilter(t *testing.T) { + for i := range anyValidChecksums { + var r eacl.Record + r.AddObjectHomomorphicHashFilter(anyValidMatcher, anyValidChecksums[i]) + assertSingleObjectFilter(t, r, "$Object:homomorphicHash", anyValidMatcher, anyValidStringChecksums[i]) + } +} - t.Run("change filters", func(t *testing.T) { - var dst Record - record.CopyTo(&dst) +func TestRecord_AddObjectTypeFilter(t *testing.T) { + for i := range anyValidObjectTypes { + var r eacl.Record + r.AddObjectTypeFilter(anyValidMatcher, anyValidObjectTypes[i]) + assertSingleObjectFilter(t, r, "$Object:objectType", anyValidMatcher, anyValidStringObjectTypes[i]) + } +} - require.Equal(t, record.filters[0].key, dst.filters[0].key) - require.Equal(t, record.filters[0].matcher, dst.filters[0].matcher) - require.Equal(t, record.filters[0].value, dst.filters[0].value) - require.Equal(t, record.filters[0].from, dst.filters[0].from) +func TestRecord_SetAction(t *testing.T) { + var r eacl.Record + require.Zero(t, r.Action()) + r.SetAction(anyValidAction) + require.Equal(t, anyValidAction, r.Action()) +} - dst.filters[0].key = "key2" - dst.filters[0].matcher = MatchStringNotEqual - dst.filters[0].value = staticStringer("staticStringer") - dst.filters[0].from = 12345 +func TestRecord_SetOperation(t *testing.T) { + var r eacl.Record + require.Zero(t, r.Operation()) + r.SetOperation(anyValidOp) + require.Equal(t, anyValidOp, r.Operation()) +} - require.NotEqual(t, record.filters[0].key, dst.filters[0].key) - require.NotEqual(t, record.filters[0].matcher, dst.filters[0].matcher) - require.NotEqual(t, record.filters[0].value, dst.filters[0].value) - require.NotEqual(t, record.filters[0].from, dst.filters[0].from) - }) +func TestRecord_SetTargets(t *testing.T) { + var r eacl.Record + require.Zero(t, r.Targets()) + r.SetTargets(anyValidTargets...) + require.Equal(t, anyValidTargets, r.Targets()) +} - t.Run("change target", func(t *testing.T) { - var dst Record - record.CopyTo(&dst) +func TestRecord_SetFilters(t *testing.T) { + var r eacl.Record + require.Zero(t, r.Filters()) + r.SetFilters(anyValidFilters) + require.Equal(t, anyValidFilters, r.Filters()) +} - require.Equal(t, record.targets[0].role, dst.targets[0].role) - dst.targets[0].role = 12345 - require.NotEqual(t, record.targets[0].role, dst.targets[0].role) +func TestConstructRecord(t *testing.T) { + r := eacl.ConstructRecord(anyValidAction, anyValidOp, anyValidTargets) + require.Equal(t, anyValidAction, r.Action()) + require.Equal(t, anyValidOp, r.Operation()) + require.Equal(t, anyValidTargets, r.Targets()) + require.Zero(t, r.Filters()) + r = eacl.ConstructRecord(anyValidAction, anyValidOp, anyValidTargets, anyValidFilters...) + require.Equal(t, anyValidFilters, r.Filters()) +} - for i, key := range dst.targets[0].keys { - require.True(t, bytes.Equal(key, record.targets[0].keys[i])) - key[0] = 10 - require.False(t, bytes.Equal(key, record.targets[0].keys[i])) - } - }) +func TestCreateRecord(t *testing.T) { + r := eacl.CreateRecord(anyValidAction, anyValidOp) + require.Equal(t, anyValidAction, r.Action()) + require.Equal(t, anyValidOp, r.Operation()) + require.Empty(t, r.Targets()) + require.Empty(t, r.Filters()) } diff --git a/eacl/table.go b/eacl/table.go index 3743d1ed..9a1d5d4a 100644 --- a/eacl/table.go +++ b/eacl/table.go @@ -1,6 +1,7 @@ package eacl import ( + "bytes" "fmt" v2acl "github.com/nspcc-dev/neofs-api-go/v2/acl" @@ -12,12 +13,41 @@ import ( // Table is a group of ContainerEACL records for single container. // // Table is compatible with v2 acl.EACLTable message. +// +// Table should be created using one of the constructors. type Table struct { version version.Version cid cid.ID records []Record } +// ConstructTable constructs new Table with given records. Use +// [NewTableForContainer] to limit the NeoFS container. The rs must not be +// empty. +func ConstructTable(rs []Record) Table { + return Table{version: version.Current(), records: rs} +} + +// NewTableForContainer constructs new Table with given records which apply only +// to the specified NeoFS container. The rs must not be empty. +func NewTableForContainer(cnr cid.ID, rs []Record) Table { + t := ConstructTable(rs) + t.SetCID(cnr) + return t +} + +// Unmarshal creates new Table and makes [Table.Unmarshal]. +func Unmarshal(b []byte) (Table, error) { + var t Table + return t, t.Unmarshal(b) +} + +// UnmarshalJSON creates new Table and makes [Table.UnmarshalJSON]. +func UnmarshalJSON(b []byte) (Table, error) { + var t Table + return t, t.UnmarshalJSON(b) +} + // CopyTo writes deep copy of the [Table] to dst. func (t Table) CopyTo(dst *Table) { ver := t.version @@ -62,7 +92,16 @@ func (t Table) Records() []Record { return t.records } +// SetRecords sets list of extended ACL rules. +// +// The value returned shares memory with the structure itself, so changing it can lead to data corruption. +// Make a copy if you need to change it. +func (t *Table) SetRecords(rs []Record) { + t.records = rs +} + // AddRecord adds single eACL rule. +// Deprecated: use [Table.SetRecords] instead. func (t *Table) AddRecord(r *Record) { if r != nil { t.records = append(t.records, *r) @@ -101,7 +140,9 @@ func (t *Table) ReadFromV2(m v2acl.Table) error { t.records = make([]Record, len(v2records)) for i := range v2records { - t.records[i] = *NewRecordFromV2(&v2records[i]) + if err := t.records[i].fromProtoMessage(&v2records[i]); err != nil { + return fmt.Errorf("invalid record #%d: %w", i, err) + } } return nil @@ -112,11 +153,7 @@ func (t *Table) ReadFromV2(m v2acl.Table) error { // Nil Table converts to nil. // // See also [Table.ReadFromV2]. -func (t *Table) ToV2() *v2acl.Table { - if t == nil { - return nil - } - +func (t Table) ToV2() *v2acl.Table { v2 := new(v2acl.Table) var cidV2 refs.ContainerID @@ -128,7 +165,7 @@ func (t *Table) ToV2() *v2acl.Table { if t.records != nil { records := make([]v2acl.Record, len(t.records)) for i := range t.records { - records[i] = *t.records[i].ToV2() + records[i] = *t.records[i].toProtoMessage() } v2.SetRecords(records) @@ -149,19 +186,18 @@ func (t *Table) ToV2() *v2acl.Table { // - records: nil; // - session token: nil; // - signature: nil. +// +// Deprecated: use [ConstructTable] instead. func NewTable() *Table { - t := new(Table) - t.SetVersion(version.Current()) - - return t + t := ConstructTable(nil) + return &t } // CreateTable creates, initializes with parameters and returns Table instance. +// Deprecated: use [NewTableForContainer] instead. func CreateTable(cid cid.ID) *Table { - t := NewTable() - t.SetCID(cid) - - return t + t := NewTableForContainer(cid, nil) + return &t } // NewTableFromV2 converts v2 acl.EACLTable message to Table. @@ -194,14 +230,14 @@ func NewTableFromV2(table *v2acl.Table) *Table { t.records = make([]Record, len(v2records)) for i := range v2records { - t.records[i] = *NewRecordFromV2(&v2records[i]) + _ = t.records[i].fromProtoMessage(&v2records[i]) } return t } // Marshal marshals Table into a protobuf binary form. -func (t *Table) Marshal() []byte { +func (t Table) Marshal() []byte { return t.ToV2().StableMarshal(nil) } @@ -212,7 +248,8 @@ func (t Table) SignedData() []byte { return t.Marshal() } -// Unmarshal unmarshals protobuf binary representation of Table. +// Unmarshal unmarshals protobuf binary representation of Table. Use [Unmarshal] +// to decode data into a new Table. func (t *Table) Unmarshal(data []byte) error { var m v2acl.Table if err := m.Unmarshal(data); err != nil { @@ -222,11 +259,12 @@ func (t *Table) Unmarshal(data []byte) error { } // MarshalJSON encodes Table to protobuf JSON format. -func (t *Table) MarshalJSON() ([]byte, error) { +func (t Table) MarshalJSON() ([]byte, error) { return t.ToV2().MarshalJSON() } -// UnmarshalJSON decodes Table from protobuf JSON format. +// UnmarshalJSON decodes Table from protobuf JSON format. Use [UnmarshalJSON] to +// decode data into a new Table. func (t *Table) UnmarshalJSON(data []byte) error { var m v2acl.Table if err := m.UnmarshalJSON(data); err != nil { @@ -236,23 +274,5 @@ func (t *Table) UnmarshalJSON(data []byte) error { } // EqualTables compares Table with each other. -func EqualTables(t1, t2 Table) bool { - if t1.GetCID() != t2.GetCID() || - !t1.Version().Equal(t2.Version()) { - return false - } - - rs1, rs2 := t1.Records(), t2.Records() - - if len(rs1) != len(rs2) { - return false - } - - for i := 0; i < len(rs1); i++ { - if !equalRecords(rs1[i], rs2[i]) { - return false - } - } - - return true -} +// Deprecated: compare [Table.Marshal] instead. +func EqualTables(t1, t2 Table) bool { return bytes.Equal(t1.Marshal(), t2.Marshal()) } diff --git a/eacl/table_test.go b/eacl/table_test.go index 2f8640d5..63f56683 100644 --- a/eacl/table_test.go +++ b/eacl/table_test.go @@ -1,104 +1,79 @@ package eacl_test import ( + "encoding/json" + "math/rand" + "strconv" "testing" + protoacl "github.com/nspcc-dev/neofs-api-go/v2/acl" "github.com/nspcc-dev/neofs-api-go/v2/refs" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" "github.com/nspcc-dev/neofs-sdk-go/eacl" - eacltest "github.com/nspcc-dev/neofs-sdk-go/eacl/test" "github.com/nspcc-dev/neofs-sdk-go/version" "github.com/stretchr/testify/require" ) -func TestTable(t *testing.T) { - id := cidtest.ID() - var v version.Version - v.SetMajor(3) - v.SetMinor(2) - - table := eacl.NewTable() - table.SetVersion(v) - table.SetCID(id) - table.AddRecord(eacl.CreateRecord(eacl.ActionAllow, eacl.OperationPut)) - - v2 := table.ToV2() - require.NotNil(t, v2) - require.Equal(t, uint32(3), v2.GetVersion().GetMajor()) - require.Equal(t, uint32(2), v2.GetVersion().GetMinor()) - require.Equal(t, id[:], v2.GetContainerID().GetValue()) - require.Len(t, v2.GetRecords(), 1) - - newTable := eacl.NewTableFromV2(v2) - require.Equal(t, table, newTable) - var res eacl.Table - require.NoError(t, res.ReadFromV2(*v2)) - require.Equal(t, table, newTable) - - t.Run("new from nil v2 table", func(t *testing.T) { - require.Equal(t, new(eacl.Table), eacl.NewTableFromV2(nil)) - }) - - t.Run("create table", func(t *testing.T) { - id := cidtest.ID() - - table := eacl.CreateTable(id) - require.Equal(t, id, table.GetCID()) - require.Equal(t, version.Current(), table.Version()) - }) +var invalidProtoEACLTestcases = []struct { + name string + err string + corrupt func(*protoacl.Table) +}{ + {name: "invalid container/nil value", err: "invalid container ID: invalid length 0", + corrupt: func(m *protoacl.Table) { m.SetContainerID(new(refs.ContainerID)) }}, + {name: "invalid container/empty value", err: "invalid container ID: invalid length 0", corrupt: func(m *protoacl.Table) { + var mc refs.ContainerID + mc.SetValue([]byte{}) + m.SetContainerID(&mc) + }}, + {name: "invalid container/undersized value", err: "invalid container ID: invalid length 31", corrupt: func(m *protoacl.Table) { + var mc refs.ContainerID + mc.SetValue(make([]byte, 31)) + m.SetContainerID(&mc) + }}, + {name: "invalid container/oversized value", err: "invalid container ID: invalid length 33", corrupt: func(m *protoacl.Table) { + var mc refs.ContainerID + mc.SetValue(make([]byte, 33)) + m.SetContainerID(&mc) + }}, } func TestTable_AddRecord(t *testing.T) { - records := []eacl.Record{ - *eacl.CreateRecord(eacl.ActionDeny, eacl.OperationDelete), - *eacl.CreateRecord(eacl.ActionAllow, eacl.OperationPut), + for i := range anyValidRecords { + var tbl eacl.Table + tbl.AddRecord(&anyValidRecords[i]) + require.Len(t, tbl.Records(), 1) + require.Equal(t, anyValidRecords[i], tbl.Records()[0]) } - - table := eacl.NewTable() - for _, record := range records { - table.AddRecord(&record) - } - - require.Equal(t, records, table.Records()) -} - -func TestTableEncoding(t *testing.T) { - tab := eacltest.Table() - - t.Run("binary", func(t *testing.T) { - tab2 := eacl.NewTable() - require.NoError(t, tab2.Unmarshal(tab.Marshal())) - - // FIXME: we compare v2 messages because - // Filter contains fmt.Stringer interface - require.Equal(t, tab.ToV2(), tab2.ToV2()) - }) - - t.Run("json", func(t *testing.T) { - data, err := tab.MarshalJSON() - require.NoError(t, err) - - tab2 := eacl.NewTable() - require.NoError(t, tab2.UnmarshalJSON(data)) - - require.Equal(t, tab.ToV2(), tab2.ToV2()) - }) } func TestTable_ToV2(t *testing.T) { - t.Run("nil", func(t *testing.T) { - var x *eacl.Table + assert := func(t testing.TB, tbl eacl.Table, m *protoacl.Table) { + require.EqualValues(t, 2, tbl.Version().Major()) + require.EqualValues(t, 16, tbl.Version().Minor()) + require.Len(t, m.GetRecords(), len(tbl.Records())) + assertProtoRecordsEqual(t, tbl.Records(), m.GetRecords()) + } - require.Nil(t, x.ToV2()) - }) + tbl := eacl.ConstructTable(anyValidRecords) + assert(t, tbl, tbl.ToV2()) + t.Run("with container", func(t *testing.T) { + cnr := cidtest.ID() + tbl = eacl.NewTableForContainer(cnr, anyValidRecords) + m := tbl.ToV2() + assert(t, tbl, m) + require.Equal(t, cnr[:], m.GetContainerID().GetValue()) + }) t.Run("default values", func(t *testing.T) { table := eacl.NewTable() // check initial values require.Equal(t, version.Current(), table.Version()) require.Nil(t, table.Records()) - require.Zero(t, table.GetCID()) + _, set := table.CID() + require.False(t, set) // convert to v2 message tableV2 := table.ToV2() @@ -136,3 +111,237 @@ func TestTable_SetCID(t *testing.T) { tbl.SetCID(cnr) require.Equal(t, cnr, tbl.GetCID()) } + +func TestNewTableFromV2(t *testing.T) { + var ver refs.Version + ver.SetMajor(rand.Uint32()) + ver.SetMinor(rand.Uint32()) + + bCnr := make([]byte, cid.Size) + //nolint:staticcheck + rand.Read(bCnr) + var cnr refs.ContainerID + cnr.SetValue(bCnr) + + rs := make([]protoacl.Record, 2) + for i := range rs { + ts := make([]protoacl.Target, 2) + for i := range ts { + ts[i].SetRole(protoacl.Role(rand.Uint32())) + ts[i].SetKeys(anyValidBinPublicKeys) + } + fs := make([]protoacl.HeaderFilter, 2) + for i := range fs { + fs[i].SetHeaderType(protoacl.HeaderType(rand.Uint32())) + fs[i].SetKey("key_" + strconv.Itoa(rand.Int())) + fs[i].SetMatchType(protoacl.MatchType(rand.Uint32())) + fs[i].SetValue("val_" + strconv.Itoa(rand.Int())) + } + rs[i].SetAction(protoacl.Action(rand.Uint32())) + rs[i].SetOperation(protoacl.Operation(rand.Uint32())) + rs[i].SetTargets(ts) + rs[i].SetFilters(fs) + } + + var m protoacl.Table + m.SetVersion(&ver) + m.SetContainerID(&cnr) + m.SetRecords(rs) + + tbl := eacl.NewTableFromV2(&m) + require.EqualValues(t, ver.GetMajor(), tbl.Version().Major()) + require.EqualValues(t, ver.GetMinor(), tbl.Version().Minor()) + resCnr, ok := tbl.CID() + require.True(t, ok) + require.Equal(t, bCnr, resCnr[:]) + assertProtoRecordsEqual(t, tbl.Records(), rs) + + t.Run("nil", func(t *testing.T) { + require.Equal(t, new(eacl.Table), eacl.NewTableFromV2(nil)) + }) +} + +func TestTable_ReadFromV2(t *testing.T) { + var ver refs.Version + ver.SetMajor(rand.Uint32()) + ver.SetMinor(rand.Uint32()) + + bCnr := make([]byte, cid.Size) + //nolint:staticcheck + rand.Read(bCnr) + var cnr refs.ContainerID + cnr.SetValue(bCnr) + + rs := make([]protoacl.Record, 2) + for i := range rs { + ts := make([]protoacl.Target, 2) + for i := range ts { + ts[i].SetRole(protoacl.Role(rand.Uint32())) + ts[i].SetKeys(anyValidBinPublicKeys) + } + fs := make([]protoacl.HeaderFilter, 2) + for i := range fs { + fs[i].SetHeaderType(protoacl.HeaderType(rand.Uint32())) + fs[i].SetKey("key_" + strconv.Itoa(rand.Int())) + fs[i].SetMatchType(protoacl.MatchType(rand.Uint32())) + fs[i].SetValue("val_" + strconv.Itoa(rand.Int())) + } + rs[i].SetAction(protoacl.Action(rand.Uint32())) + rs[i].SetOperation(protoacl.Operation(rand.Uint32())) + rs[i].SetTargets(ts) + rs[i].SetFilters(fs) + } + + var m protoacl.Table + m.SetVersion(&ver) + m.SetContainerID(&cnr) + m.SetRecords(rs) + + var tbl eacl.Table + require.NoError(t, tbl.ReadFromV2(m)) + require.EqualValues(t, ver.GetMajor(), tbl.Version().Major()) + require.EqualValues(t, ver.GetMinor(), tbl.Version().Minor()) + require.EqualValues(t, bCnr, tbl.GetCID()) + assertProtoRecordsEqual(t, tbl.Records(), rs) + + m.SetContainerID(nil) + require.NoError(t, tbl.ReadFromV2(m)) + require.Zero(t, tbl.GetCID()) + + t.Run("invalid", func(t *testing.T) { + for _, tc := range invalidProtoEACLTestcases { + t.Run(tc.name, func(t *testing.T) { + m := anyValidEACL.ToV2() + require.NotNil(t, m) + tc.corrupt(m) + require.EqualError(t, new(eacl.Table).ReadFromV2(*m), tc.err) + }) + } + }) +} + +func TestTable_SignedData(t *testing.T) { + require.Equal(t, anyValidEACLBytes, anyValidEACL.SignedData()) +} + +func TestTable_Marshal(t *testing.T) { + require.Equal(t, anyValidEACLBytes, anyValidEACL.Marshal()) +} + +func testUnmarshalTableFunc(t *testing.T, f func(*eacl.Table, []byte) error) { + t.Run("invalid protobuf", func(t *testing.T) { + err := f(new(eacl.Table), []byte("Hello, world!")) + require.ErrorContains(t, err, "proto") + require.ErrorContains(t, err, "cannot parse invalid wire-format data") + }) + + var tbl eacl.Table + require.NoError(t, f(&tbl, anyValidEACLBytes)) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/606") + require.Equal(t, anyValidEACL, tbl) +} + +func TestTable_Unmarshal(t *testing.T) { + testUnmarshalTableFunc(t, (*eacl.Table).Unmarshal) +} + +func TestUnmarshal(t *testing.T) { + testUnmarshalTableFunc(t, func(tbl *eacl.Table, b []byte) error { + res, err := eacl.Unmarshal(b) + if err == nil { + *tbl = res + } + return err + }) +} + +func TestTable_MarshalJSON(t *testing.T) { + var tbl1 eacl.Table + b, err := anyValidEACL.MarshalJSON() + require.NoError(t, err) + require.NoError(t, tbl1.UnmarshalJSON(b)) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/606") + require.Equal(t, anyValidEACL, tbl1) + + var tbl2 eacl.Table + b, err = json.Marshal(anyValidEACL) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(b, &tbl2)) + require.Equal(t, anyValidEACL, tbl2) +} + +func testUnmarshalTableJSONFunc(t *testing.T, f func(*eacl.Table, []byte) error) { + t.Run("invalid JSON", func(t *testing.T) { + err := f(new(eacl.Table), []byte("Hello, world!")) + require.ErrorContains(t, err, "proto") + require.ErrorContains(t, err, "syntax error") + }) + + var tbl eacl.Table + require.NoError(t, f(&tbl, []byte(anyValidEACLJSON))) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/606") + require.Equal(t, anyValidEACL, tbl) +} + +func TestTable_UnmarshalJSON(t *testing.T) { + testUnmarshalTableJSONFunc(t, (*eacl.Table).UnmarshalJSON) + + var tbl eacl.Table + require.NoError(t, json.Unmarshal([]byte(anyValidEACLJSON), &tbl)) + require.Equal(t, anyValidEACL, tbl) + + tbl3, err := eacl.UnmarshalJSON([]byte(anyValidEACLJSON)) + require.NoError(t, err) + require.Equal(t, anyValidEACL, tbl3) +} + +func TestUnmarshalJSON(t *testing.T) { + testUnmarshalTableJSONFunc(t, func(tbl *eacl.Table, b []byte) error { + res, err := eacl.UnmarshalJSON(b) + if err == nil { + *tbl = res + } + return err + }) + + tbl, err := eacl.UnmarshalJSON([]byte(anyValidEACLJSON)) + require.NoError(t, err) + require.Equal(t, anyValidEACL, tbl) +} + +func TestSetRecords(t *testing.T) { + var tbl eacl.Table + require.Zero(t, tbl.Records()) + tbl.SetRecords(anyValidRecords) + require.Equal(t, anyValidRecords, tbl.Records()) +} + +func TestConstructTable(t *testing.T) { + tbl := eacl.ConstructTable(anyValidRecords) + require.Equal(t, anyValidRecords, tbl.Records()) + _, ok := tbl.CID() + require.False(t, ok) + require.EqualValues(t, 2, tbl.Version().Major()) + require.EqualValues(t, 16, tbl.Version().Minor()) +} + +func TestNewTableForContainer(t *testing.T) { + cnr := cidtest.ID() + tbl := eacl.NewTableForContainer(cnr, anyValidRecords) + require.Equal(t, anyValidRecords, tbl.Records()) + cnr2, ok := tbl.CID() + require.True(t, ok) + require.Equal(t, cnr, cnr2) + require.EqualValues(t, 2, tbl.Version().Major()) + require.EqualValues(t, 16, tbl.Version().Minor()) +} + +func TestCreateTable(t *testing.T) { + tbl := eacl.CreateTable(anyValidContainerID) + require.EqualValues(t, 2, tbl.Version().Major()) + require.EqualValues(t, 16, tbl.Version().Minor()) + cnr, ok := tbl.CID() + require.True(t, ok) + require.Equal(t, anyValidContainerID, cnr) + require.Zero(t, tbl.Records()) +} diff --git a/eacl/target.go b/eacl/target.go index faddf3b5..b9a194fa 100644 --- a/eacl/target.go +++ b/eacl/target.go @@ -10,13 +10,37 @@ import ( "github.com/nspcc-dev/neofs-sdk-go/user" ) -// Target is a group of request senders to match ContainerEACL. Defined by role enum -// and set of public keys. +// Target describes the NeoFS parties that are subject to a specific access +// rule. // -// Target is compatible with v2 acl.EACLRecord.Target message. +// Target should be created using one of the constructors. type Target struct { - role Role - keys [][]byte + role Role + subjs [][]byte +} + +// NewTargetByRole returns Target for specified role. Use NewTargetByRole in [Record] +// to direct it to subjects with the given role in NeoFS. +func NewTargetByRole(role Role) Target { return Target{role: role} } + +// NewTargetByAccounts returns Target for specified set of NeoFS accounts. Use +// NewTargetByAccounts in [Record] to direct access rule to the given subjects in +// NeoFS. +func NewTargetByAccounts(accs []user.ID) Target { + var res Target + res.SetAccounts(accs) + return res +} + +// NewTargetByScriptHashes is an alternative to [NewTargetByAccounts] which +// allows to pass accounts as their script hashes. +func NewTargetByScriptHashes(hs []util.Uint160) Target { + b := make([][]byte, len(hs)) + for i := range hs { + h := user.NewFromScriptHash(hs[i]) + b[i] = h[:] + } + return Target{subjs: b} } func ecdsaKeysToPtrs(keys []ecdsa.PublicKey) []*ecdsa.PublicKey { @@ -33,9 +57,9 @@ func ecdsaKeysToPtrs(keys []ecdsa.PublicKey) []*ecdsa.PublicKey { func (t Target) CopyTo(dst *Target) { dst.role = t.role - dst.keys = make([][]byte, len(t.keys)) - for i := range t.keys { - dst.keys[i] = bytes.Clone(t.keys[i]) + dst.subjs = make([][]byte, len(t.subjs)) + for i := range t.subjs { + dst.subjs[i] = bytes.Clone(t.subjs[i]) } } @@ -51,7 +75,7 @@ func (t Target) CopyTo(dst *Target) { func (t *Target) BinaryKeys() [][]byte { var r [][]byte - for _, key := range t.keys { + for _, key := range t.subjs { if len(key) == 33 { r = append(r, key) } @@ -60,6 +84,15 @@ func (t *Target) BinaryKeys() [][]byte { return r } +// SetRawSubjects sets target subjects in a binary format. Each element must be +// either 25-byte NeoFS user ID (see [user.ID]) or 33-byte compressed ECDSA +// public key. Use constructors to work with particular types. SetRawSubjects +// should only be used if you do not want to decode the data and take +// responsibility for its correctness. +func (t *Target) SetRawSubjects(subjs [][]byte) { + t.subjs = subjs +} + // RawSubjects returns list of public keys or [user.ID] to identify target subject in a binary format. // // If element length is 33, it is a serialized compressed public key. See [elliptic.MarshalCompressed], [keys.PublicKey.GetScriptHash]. @@ -67,7 +100,7 @@ func (t *Target) BinaryKeys() [][]byte { // // Using this method is your responsibility. func (t Target) RawSubjects() [][]byte { - return t.keys + return t.subjs } // SetBinaryKeys sets list of binary public keys to identify @@ -76,9 +109,7 @@ func (t Target) RawSubjects() [][]byte { // Each element of the keys parameter is a slice of bytes is a serialized compressed public key. // See [elliptic.MarshalCompressed]. // Deprecated: use [Target.SetAccounts] instead. -func (t *Target) SetBinaryKeys(keys [][]byte) { - t.keys = keys -} +func (t *Target) SetBinaryKeys(keys [][]byte) { t.SetRawSubjects(keys) } // Accounts returns list of accounts to identify target subject. // @@ -86,7 +117,7 @@ func (t *Target) SetBinaryKeys(keys [][]byte) { func (t Target) Accounts() []user.ID { var r []user.ID - for _, key := range t.keys { + for _, key := range t.subjs { if len(key) == user.IDSize { r = append(r, user.ID(key)) } @@ -97,15 +128,18 @@ func (t Target) Accounts() []user.ID { // SetAccounts sets list of accounts to identify target subject. func (t *Target) SetAccounts(accounts []user.ID) { - t.keys = make([][]byte, len(accounts)) + subjs := make([][]byte, len(accounts)) for i, acc := range accounts { - t.keys[i] = bytes.Clone(acc[:]) + subjs[i] = bytes.Clone(acc[:]) } + t.SetRawSubjects(subjs) } -// SetTargetECDSAKeys converts ECDSA public keys to a binary -// format and stores them in Target. +// SetTargetECDSAKeys converts ECDSA public keys to a binary format and stores +// them in Target. +// Deprecated: use [NewTargetByAccounts] or [Target.SetAccounts] along with +// [user.NewFromECDSAPublicKey] instead. func SetTargetECDSAKeys(t *Target, pubs ...*ecdsa.PublicKey) { binKeys := t.BinaryKeys() ln := len(pubs) @@ -124,6 +158,7 @@ func SetTargetECDSAKeys(t *Target, pubs ...*ecdsa.PublicKey) { } // SetTargetAccounts sets accounts in Target. +// Deprecated: use [NewTargetByScriptHashes] instead. func SetTargetAccounts(t *Target, accs ...util.Uint160) { account := make([]user.ID, len(accs)) ln := len(accs) @@ -138,6 +173,7 @@ func SetTargetAccounts(t *Target, accs ...util.Uint160) { // TargetECDSAKeys interprets binary public keys of Target // as ECDSA public keys. If any key has a different format, // the corresponding element will be nil. +// Deprecated: use [Target.RawSubjects] with [keys.PublicKey.DecodeBytes] instead. func TargetECDSAKeys(t *Target) []*ecdsa.PublicKey { binKeys := t.BinaryKeys() ln := len(binKeys) @@ -167,90 +203,69 @@ func (t Target) Role() Role { // ToV2 converts Target to v2 acl.EACLRecord.Target message. // // Nil Target converts to nil. +// Deprecated: do not use it. func (t *Target) ToV2() *v2acl.Target { - if t == nil { - return nil + if t != nil { + return t.toProtoMessage() } + return nil +} +func (t Target) toProtoMessage() *v2acl.Target { target := new(v2acl.Target) - target.SetRole(t.role.ToV2()) - target.SetKeys(t.keys) + target.SetRole(v2acl.Role(t.role)) + target.SetKeys(t.subjs) return target } +func (t *Target) fromProtoMessage(m *v2acl.Target) error { + t.role = Role(m.GetRole()) + t.subjs = m.GetKeys() + return nil +} + // NewTarget creates, initializes and returns blank Target instance. // // Defaults: -// - role: RoleUnknown; +// - role: RoleUnspecified; // - keys: nil. -func NewTarget() *Target { - return NewTargetFromV2(new(v2acl.Target)) -} +// +// Deprecated: use [NewTargetByRole] or [TargetByPublicKeys] instead. +func NewTarget() *Target { return new(Target) } // NewTargetFromV2 converts v2 acl.EACLRecord.Target message to Target. +// Deprecated: do not use it. func NewTargetFromV2(target *v2acl.Target) *Target { - if target == nil { - return new(Target) - } - - return &Target{ - role: RoleFromV2(target.GetRole()), - keys: target.GetKeys(), - } + t := new(Target) + _ = t.fromProtoMessage(target) + return t } // Marshal marshals Target into a protobuf binary form. -func (t *Target) Marshal() []byte { - return t.ToV2().StableMarshal(nil) +func (t Target) Marshal() []byte { + return t.toProtoMessage().StableMarshal(nil) } // Unmarshal unmarshals protobuf binary representation of Target. func (t *Target) Unmarshal(data []byte) error { - fV2 := new(v2acl.Target) - if err := fV2.Unmarshal(data); err != nil { + m := new(v2acl.Target) + if err := m.Unmarshal(data); err != nil { return err } - - *t = *NewTargetFromV2(fV2) - - return nil + return t.fromProtoMessage(m) } // MarshalJSON encodes Target to protobuf JSON format. -func (t *Target) MarshalJSON() ([]byte, error) { - return t.ToV2().MarshalJSON() +func (t Target) MarshalJSON() ([]byte, error) { + return t.toProtoMessage().MarshalJSON() } // UnmarshalJSON decodes Target from protobuf JSON format. func (t *Target) UnmarshalJSON(data []byte) error { - tV2 := new(v2acl.Target) - if err := tV2.UnmarshalJSON(data); err != nil { + m := new(v2acl.Target) + if err := m.UnmarshalJSON(data); err != nil { return err } - - *t = *NewTargetFromV2(tV2) - - return nil -} - -// equalTargets compares Target with each other. -func equalTargets(t1, t2 Target) bool { - if t1.Role() != t2.Role() { - return false - } - - keys1, keys2 := t1.BinaryKeys(), t2.BinaryKeys() - - if len(keys1) != len(keys2) { - return false - } - - for i := 0; i < len(keys1); i++ { - if !bytes.Equal(keys1[i], keys2[i]) { - return false - } - } - - return true + return t.fromProtoMessage(m) } diff --git a/eacl/target_internal_test.go b/eacl/target_internal_test.go new file mode 100644 index 00000000..5d3ea13b --- /dev/null +++ b/eacl/target_internal_test.go @@ -0,0 +1,38 @@ +package eacl + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestTarget_CopyTo(t *testing.T) { + var target Target + target.SetRole(1) + target.SetBinaryKeys([][]byte{ + {1, 2, 3}, + }) + + t.Run("copy", func(t *testing.T) { + var dst Target + target.CopyTo(&dst) + + require.Equal(t, target, dst) + require.True(t, bytes.Equal(target.Marshal(), dst.Marshal())) + }) + + t.Run("change", func(t *testing.T) { + var dst Target + target.CopyTo(&dst) + + require.Equal(t, target.role, dst.role) + dst.SetRole(2) + require.NotEqual(t, target.role, dst.role) + + require.True(t, bytes.Equal(target.subjs[0], dst.subjs[0])) + // change some key data + dst.subjs[0][0] = 5 + require.False(t, bytes.Equal(target.subjs[0], dst.subjs[0])) + }) +} diff --git a/eacl/target_test.go b/eacl/target_test.go index 260f463f..e6632d19 100644 --- a/eacl/target_test.go +++ b/eacl/target_test.go @@ -1,166 +1,246 @@ -package eacl +package eacl_test import ( - "bytes" - "crypto/ecdsa" + "encoding/json" + "math/rand" "testing" - "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/util" - "github.com/nspcc-dev/neofs-api-go/v2/acl" - v2acl "github.com/nspcc-dev/neofs-api-go/v2/acl" + protoacl "github.com/nspcc-dev/neofs-api-go/v2/acl" + "github.com/nspcc-dev/neofs-sdk-go/eacl" "github.com/nspcc-dev/neofs-sdk-go/user" usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" "github.com/stretchr/testify/require" ) -func TestTarget(t *testing.T) { - pubs := []*ecdsa.PublicKey{ - randomPublicKey(t), - randomPublicKey(t), +func TestTarget_ToV2(t *testing.T) { + r := eacl.NewTargetByRole(anyValidRole) + subjs := [][]byte{ + anyValidECDSABinPublicKeys[0], + anyUserSet[0][:], + anyValidECDSABinPublicKeys[1], + anyUserSet[1][:], + anyUserSet[2][:], } + r.SetRawSubjects(subjs) + m := r.ToV2() + require.EqualValues(t, anyValidRole, m.GetRole()) + require.Equal(t, subjs, m.GetKeys()) - target := NewTarget() - target.SetRole(RoleSystem) - SetTargetECDSAKeys(target, pubs...) + t.Run("default values", func(t *testing.T) { + target := eacl.NewTarget() - v2 := target.ToV2() - require.NotNil(t, v2) - require.Equal(t, v2acl.RoleSystem, v2.GetRole()) - require.Len(t, v2.GetKeys(), len(pubs)) - for i, key := range v2.GetKeys() { - require.Equal(t, key, (*keys.PublicKey)(pubs[i]).Bytes()) - } + // check initial values + require.Zero(t, target.Role()) + require.Nil(t, target.BinaryKeys()) - newTarget := NewTargetFromV2(v2) - require.Equal(t, target, newTarget) + // convert to v2 message + targetV2 := target.ToV2() - t.Run("from nil v2 target", func(t *testing.T) { - require.Equal(t, new(Target), NewTargetFromV2(nil)) + require.Equal(t, protoacl.RoleUnknown, targetV2.GetRole()) + require.Nil(t, targetV2.GetKeys()) }) } -func TestTargetAccounts(t *testing.T) { - accs := []util.Uint160{ - (*keys.PublicKey)(randomPublicKey(t)).GetScriptHash(), - (*keys.PublicKey)(randomPublicKey(t)).GetScriptHash(), - } - - target := NewTarget() - target.SetRole(RoleSystem) - SetTargetAccounts(target, accs...) - - v2 := target.ToV2() - require.NotNil(t, v2) - require.Equal(t, v2acl.RoleSystem, v2.GetRole()) - require.Len(t, v2.GetKeys(), len(accs)) - for i, key := range v2.GetKeys() { - var u = user.NewFromScriptHash(accs[i]) - require.Equal(t, key, u[:]) - } +func TestNewTargetFromV2(t *testing.T) { + role := protoacl.Role(rand.Uint32()) + var m protoacl.Target + m.SetRole(role) + m.SetKeys(anyValidBinPublicKeys) - newTarget := NewTargetFromV2(v2) - require.Equal(t, target, newTarget) + r := eacl.NewTargetFromV2(&m) + require.EqualValues(t, role, r.Role()) + require.Equal(t, anyValidBinPublicKeys, m.GetKeys()) - t.Run("from nil v2 target", func(t *testing.T) { - require.Equal(t, new(Target), NewTargetFromV2(nil)) + t.Run("nil", func(t *testing.T) { + require.Equal(t, new(eacl.Target), eacl.NewTargetFromV2(nil)) }) } -func TestTargetUsers(t *testing.T) { - accs := usertest.IDs(2) +func TestTarget_Marshal(t *testing.T) { + for i := range anyValidTargets { + require.Equal(t, anyValidBinTargets[i], anyValidTargets[i].Marshal()) + } +} - target := NewTarget() - target.SetRole(RoleSystem) - target.SetAccounts(accs) +func TestTarget_Unmarshal(t *testing.T) { + t.Run("invalid protobuf", func(t *testing.T) { + err := new(eacl.Target).Unmarshal([]byte("Hello, world!")) + require.ErrorContains(t, err, "proto") + require.ErrorContains(t, err, "cannot parse invalid wire-format data") + }) - v2 := target.ToV2() - require.NotNil(t, v2) - require.Equal(t, v2acl.RoleSystem, v2.GetRole()) - require.Len(t, v2.GetKeys(), len(accs)) - for i, key := range v2.GetKeys() { - require.Equal(t, key, accs[i][:]) + var tgt eacl.Target + for i := range anyValidBinTargets { + err := tgt.Unmarshal(anyValidBinTargets[i]) + require.NoError(t, err) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/606") + require.Equal(t, anyValidTargets[i], tgt) } +} - newTarget := NewTargetFromV2(v2) - require.Equal(t, target, newTarget) - - t.Run("from nil v2 target", func(t *testing.T) { - require.Equal(t, new(Target), NewTargetFromV2(nil)) +func TestTarget_MarshalJSON(t *testing.T) { + t.Run("invalid JSON", func(t *testing.T) { + err := new(eacl.Target).UnmarshalJSON([]byte("Hello, world!")) + require.ErrorContains(t, err, "proto") + require.ErrorContains(t, err, "syntax error") }) -} -func TestTargetEncoding(t *testing.T) { - tar := NewTarget() - tar.SetRole(RoleSystem) - SetTargetECDSAKeys(tar, randomPublicKey(t)) + var tgt1, tgt2 eacl.Target + for i := range anyValidTargets { + b, err := anyValidTargets[i].MarshalJSON() + require.NoError(t, err, i) + require.NoError(t, tgt1.UnmarshalJSON(b), i) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/606") + require.Equal(t, anyValidTargets[i], tgt1, i) + + b, err = json.Marshal(anyValidTargets[i]) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(b, &tgt2), i) + require.Equal(t, anyValidTargets[i], tgt2, i) + } +} - t.Run("binary", func(t *testing.T) { - tar2 := NewTarget() - require.NoError(t, tar2.Unmarshal(tar.Marshal())) +func TestTarget_UnmarshalJSON(t *testing.T) { + var tgt1, tgt2 eacl.Target + for i := range anyValidJSONTargets { + require.NoError(t, tgt1.UnmarshalJSON([]byte(anyValidJSONTargets[i])), i) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/606") + require.Equal(t, anyValidTargets[i], tgt1, i) - require.Equal(t, tar, tar2) - }) + require.NoError(t, json.Unmarshal([]byte(anyValidJSONTargets[i]), &tgt2), i) + require.Equal(t, anyValidTargets[i], tgt2, i) + } +} - t.Run("json", func(t *testing.T) { - data, err := tar.MarshalJSON() - require.NoError(t, err) +func TestTarget_SetRole(t *testing.T) { + var tgt eacl.Target + require.Zero(t, tgt.Role()) - tar2 := NewTarget() - require.NoError(t, tar2.UnmarshalJSON(data)) + tgt.SetRole(anyValidRole) + require.Equal(t, anyValidRole, tgt.Role()) - require.Equal(t, tar, tar2) - }) + otherRole := anyValidRole + 1 + tgt.SetRole(otherRole) + require.Equal(t, otherRole, tgt.Role()) } -func TestTarget_ToV2(t *testing.T) { - t.Run("nil", func(t *testing.T) { - var x *Target +func TestTarget_SetBinaryKeys(t *testing.T) { + var tgt eacl.Target + require.Zero(t, tgt.BinaryKeys()) - require.Nil(t, x.ToV2()) - }) + ks := make([][]byte, 3) + for i := range ks { + ks[i] = make([]byte, 33) + //nolint:staticcheck + rand.Read(ks[i]) + } + tgt.SetBinaryKeys(ks) + require.Equal(t, ks, tgt.BinaryKeys()) + + otherKeys := make([][]byte, 3) + for i := range otherKeys { + otherKeys[i] = make([]byte, 33) + //nolint:staticcheck + rand.Read(otherKeys[i]) + } + tgt.SetBinaryKeys(otherKeys) + require.Equal(t, otherKeys, tgt.BinaryKeys()) +} - t.Run("default values", func(t *testing.T) { - target := NewTarget() +func TestTargetByRole(t *testing.T) { + tgt := eacl.NewTargetByRole(anyValidRole) + require.Equal(t, anyValidRole, tgt.Role()) + require.Zero(t, tgt.Accounts()) +} - // check initial values - require.Equal(t, RoleUnknown, target.Role()) - require.Nil(t, target.BinaryKeys()) +func TestNewTargetByAccounts(t *testing.T) { + accs := usertest.IDs(5) + tgt := eacl.NewTargetByAccounts(accs) + require.Equal(t, accs, tgt.Accounts()) + require.Zero(t, tgt.Role()) +} - // convert to v2 message - targetV2 := target.ToV2() +func randomScriptHashes(n int) []util.Uint160 { + hs := make([]util.Uint160, n) + for i := range hs { + //nolint:staticcheck + rand.Read(hs[i][:]) + } + return hs +} - require.Equal(t, acl.RoleUnknown, targetV2.GetRole()) - require.Nil(t, targetV2.GetKeys()) - }) +func assertUsersMatchScriptHashes(t testing.TB, usrs []user.ID, hs []util.Uint160) { + require.Len(t, usrs, len(hs)) + for i := range usrs { + require.EqualValues(t, 0x35, usrs[i][0]) + require.Equal(t, hs[i][:], usrs[i][1:21]) + require.Equal(t, hash.Checksum(usrs[i][:21])[:4], usrs[i][21:]) + } } -func TestTarget_CopyTo(t *testing.T) { - var target Target - target.SetRole(1) - target.SetBinaryKeys([][]byte{ - {1, 2, 3}, - }) +func TestNewTargetByScriptHashes(t *testing.T) { + hs := randomScriptHashes(3) + tgt := eacl.NewTargetByScriptHashes(hs) + assertUsersMatchScriptHashes(t, tgt.Accounts(), hs) +} - t.Run("copy", func(t *testing.T) { - var dst Target - target.CopyTo(&dst) +func TestSetTargetAccounts(t *testing.T) { + hs := randomScriptHashes(3) + var tgt eacl.Target + eacl.SetTargetAccounts(&tgt, hs...) + assertUsersMatchScriptHashes(t, tgt.Accounts(), hs) +} - require.Equal(t, target, dst) - require.True(t, bytes.Equal(target.Marshal(), dst.Marshal())) - }) +func TestSetTargetECDSAKeys(t *testing.T) { + var tgt eacl.Target + require.Zero(t, tgt.BinaryKeys()) + eacl.SetTargetECDSAKeys(&tgt) + require.Zero(t, tgt.BinaryKeys()) - t.Run("change", func(t *testing.T) { - var dst Target - target.CopyTo(&dst) + eacl.SetTargetECDSAKeys(&tgt, anyECDSAPublicKeysPtr...) + require.Equal(t, anyValidECDSABinPublicKeys, tgt.BinaryKeys()) +} - require.Equal(t, target.role, dst.role) - dst.SetRole(2) - require.NotEqual(t, target.role, dst.role) +func TestTargetECDSAKeys(t *testing.T) { + var tgt eacl.Target + require.Empty(t, eacl.TargetECDSAKeys(&tgt)) - require.True(t, bytes.Equal(target.keys[0], dst.keys[0])) - // change some key data - dst.keys[0][0] = 5 - require.False(t, bytes.Equal(target.keys[0], dst.keys[0])) - }) + tgt.SetBinaryKeys(anyValidECDSABinPublicKeys) + require.Equal(t, anyECDSAPublicKeysPtr, eacl.TargetECDSAKeys(&tgt)) +} + +func TestTarget_SetRawSubjects(t *testing.T) { + var tgt eacl.Target + require.Zero(t, tgt.RawSubjects()) + require.Zero(t, tgt.Accounts()) + require.Zero(t, tgt.BinaryKeys()) + + garbageSubjs := [][]byte{[]byte("foo"), []byte("bar")} + tgt.SetRawSubjects(garbageSubjs) + require.Equal(t, garbageSubjs, tgt.RawSubjects()) + require.Zero(t, tgt.Accounts()) + require.Zero(t, tgt.BinaryKeys()) + + subjs := [][]byte{ + garbageSubjs[0], + make([]byte, 33), + nil, + garbageSubjs[1], + nil, + make([]byte, 33), + } + //nolint:staticcheck + rand.Read(subjs[1]) + //nolint:staticcheck + rand.Read(subjs[5]) + usrs := usertest.IDs(2) + subjs[2] = usrs[0][:] + subjs[4] = usrs[1][:] + + tgt.SetRawSubjects(subjs) + require.Equal(t, subjs, tgt.RawSubjects()) + require.Equal(t, usrs, tgt.Accounts()) + require.Equal(t, [][]byte{subjs[1], subjs[5]}, tgt.BinaryKeys()) } diff --git a/eacl/test/benchmark_test.go b/eacl/test/benchmark_test.go index d4dce479..b52653cb 100644 --- a/eacl/test/benchmark_test.go +++ b/eacl/test/benchmark_test.go @@ -7,7 +7,6 @@ import ( cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" "github.com/nspcc-dev/neofs-sdk-go/eacl" - versiontest "github.com/nspcc-dev/neofs-sdk-go/version/test" "github.com/stretchr/testify/require" ) @@ -27,7 +26,7 @@ func baseBenchmarkTableBinaryComparison(b *testing.B, factor int) { func baseBenchmarkTableEqualsComparison(b *testing.B, factor int) { t := TableN(factor) - t2 := eacl.NewTable() + var t2 eacl.Table err := t2.Unmarshal(t.Marshal()) require.NoError(b, err) @@ -35,7 +34,7 @@ func baseBenchmarkTableEqualsComparison(b *testing.B, factor int) { b.ResetTimer() b.StartTimer() for i := 0; i < b.N; i++ { - if !eacl.EqualTables(*t, *t2) { + if !eacl.EqualTables(*t, t2) { b.Fail() } } @@ -67,7 +66,7 @@ func BenchmarkTableEqualsComparison100(b *testing.B) { // Target returns random eacl.Target. func TargetN(n int) *eacl.Target { - x := eacl.NewTarget() + var x eacl.Target x.SetRole(eacl.RoleSystem) keys := make([][]byte, n) @@ -80,34 +79,25 @@ func TargetN(n int) *eacl.Target { x.SetBinaryKeys(keys) - return x + return &x } // Record returns random eacl.Record. func RecordN(n int) *eacl.Record { - x := eacl.NewRecord() - - x.SetAction(eacl.ActionAllow) - x.SetOperation(eacl.OperationRangeHash) - x.SetTargets(*TargetN(n)) - + fs := make([]eacl.Filter, n) for i := 0; i < n; i++ { - x.AddFilter(eacl.HeaderFromObject, eacl.MatchStringEqual, "", cidtest.ID().EncodeToString()) + fs[i] = eacl.ConstructFilter(eacl.HeaderFromObject, "", eacl.MatchStringEqual, cidtest.ID().EncodeToString()) } - return x + x := eacl.ConstructRecord(eacl.ActionAllow, eacl.OperationRangeHash, []eacl.Target{*TargetN(n)}, fs...) + return &x } func TableN(n int) *eacl.Table { - x := eacl.NewTable() - - x.SetCID(cidtest.ID()) - + rs := make([]eacl.Record, n) for i := 0; i < n; i++ { - x.AddRecord(RecordN(n)) + rs[i] = *RecordN(n) } - - x.SetVersion(versiontest.Version()) - - return x + x := eacl.NewTableForContainer(cidtest.ID(), rs) + return &x } diff --git a/eacl/test/generate.go b/eacl/test/generate.go index 31d0cf34..17dd5af8 100644 --- a/eacl/test/generate.go +++ b/eacl/test/generate.go @@ -1,48 +1,64 @@ package eacltest import ( + "math/rand" + "strconv" + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" "github.com/nspcc-dev/neofs-sdk-go/eacl" usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" - versiontest "github.com/nspcc-dev/neofs-sdk-go/version/test" ) // Target returns random eacl.Target. func Target() eacl.Target { - x := eacl.NewTarget() + if rand.Int()%2 == 0 { + return eacl.NewTargetByRole(eacl.Role(rand.Uint32())) + } + return eacl.NewTargetByAccounts(usertest.IDs(1 + rand.Intn(10))) +} - x.SetRole(eacl.RoleSystem) - x.SetBinaryKeys([][]byte{ - {1, 2, 3}, - {4, 5, 6}, - }) +// Targets returns n random eacl.Target instances. +func Targets(n int) []eacl.Target { + res := make([]eacl.Target, n) + for i := range res { + res[i] = Target() + } + return res +} + +// Filter returns random eacl.Filter. +func Filter() eacl.Filter { + si := strconv.Itoa(rand.Int()) + return eacl.ConstructFilter(eacl.FilterHeaderType(rand.Uint32()), "key_"+si, eacl.Match(rand.Uint32()), "val_"+si) +} - return *x +// Filters returns n random eacl.Filter instances. +func Filters(n int) []eacl.Filter { + res := make([]eacl.Filter, n) + for i := range res { + res[i] = Filter() + } + return res } // Record returns random eacl.Record. func Record() eacl.Record { - x := eacl.NewRecord() - - x.SetAction(eacl.ActionAllow) - x.SetOperation(eacl.OperationRangeHash) - x.SetTargets(Target(), Target()) - x.AddObjectContainerIDFilter(eacl.MatchStringEqual, cidtest.ID()) - usr := usertest.ID() - x.AddObjectOwnerIDFilter(eacl.MatchStringNotEqual, &usr) + tn := 1 + rand.Intn(10) + fn := 1 + rand.Intn(10) + return eacl.ConstructRecord(eacl.Action(rand.Uint32()), eacl.Operation(rand.Uint32()), Targets(tn), Filters(fn)...) +} - return *x +// Records returns n random eacl.Record instances. +func Records(n int) []eacl.Record { + res := make([]eacl.Record, n) + for i := range res { + res[i] = Record() + } + return res } +// Table returns random eacl.Table. func Table() eacl.Table { - x := eacl.NewTable() - - x.SetCID(cidtest.ID()) - r1 := Record() - x.AddRecord(&r1) - r2 := Record() - x.AddRecord(&r2) - x.SetVersion(versiontest.Version()) - - return *x + n := 1 + rand.Intn(10) + return eacl.NewTableForContainer(cidtest.ID(), Records(n)) } diff --git a/eacl/test/generate_test.go b/eacl/test/generate_test.go new file mode 100644 index 00000000..66c259cf --- /dev/null +++ b/eacl/test/generate_test.go @@ -0,0 +1,57 @@ +package eacltest_test + +import ( + "math/rand" + "testing" + + "github.com/nspcc-dev/neofs-sdk-go/eacl" + eacltest "github.com/nspcc-dev/neofs-sdk-go/eacl/test" + "github.com/stretchr/testify/require" +) + +func TestTarget(t *testing.T) { + require.NotEqual(t, eacltest.Target(), eacltest.Target()) +} + +func TestTargets(t *testing.T) { + n := rand.Int() % 10 + require.Len(t, eacltest.Targets(n), n) +} + +func TestFilter(t *testing.T) { + require.NotEqual(t, eacltest.Filter(), eacltest.Filter()) +} + +func TestFilters(t *testing.T) { + n := rand.Int() % 10 + require.Len(t, eacltest.Filters(n), n) +} + +func TestRecord(t *testing.T) { + require.NotEqual(t, eacltest.Record(), eacltest.Record()) +} + +func TestRecords(t *testing.T) { + n := rand.Int() % 10 + require.Len(t, eacltest.Records(n), n) +} + +func TestTable(t *testing.T) { + eACL := eacltest.Table() + require.NotEqual(t, eACL, eacltest.Table()) + + var eACL2 eacl.Table + require.NoError(t, eACL2.ReadFromV2(*eACL.ToV2())) + require.Equal(t, eACL, eACL2) + + var eACL3 eacl.Table + require.NoError(t, eACL3.Unmarshal(eACL.Marshal())) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/606") + require.Equal(t, eACL, eACL3) + + j, err := eACL.MarshalJSON() + require.NoError(t, err) + var eACL4 eacl.Table + require.NoError(t, eACL4.UnmarshalJSON(j)) + require.Equal(t, eACL, eACL4) +} diff --git a/eacl/types_test.go b/eacl/types_test.go new file mode 100644 index 00000000..bc51d6a1 --- /dev/null +++ b/eacl/types_test.go @@ -0,0 +1,49 @@ +package eacl + +import ( + "math/rand" + "testing" + + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" + "github.com/stretchr/testify/require" +) + +func TestValidationUnit_WithContainerID(t *testing.T) { + cnr := cidtest.ID() + u := new(ValidationUnit).WithContainerID(&cnr) + require.Equal(t, cnr, *u.cid) +} + +func TestValidationUnit_WithRole(t *testing.T) { + role := Role(rand.Uint32()) + u := new(ValidationUnit).WithRole(role) + require.Equal(t, role, u.role) +} + +func TestValidationUnit_WithOperation(t *testing.T) { + op := Operation(rand.Uint32()) + u := new(ValidationUnit).WithOperation(op) + require.Equal(t, op, u.op) +} + +func TestValidationUnit_WithHeaderSource(t *testing.T) { + hdrs := new(headers) + u := new(ValidationUnit).WithHeaderSource(hdrs) + require.Equal(t, hdrs, u.hdrSrc) +} + +func TestValidationUnit_WithSenderKey(t *testing.T) { + key := []byte("any_key") + u := new(ValidationUnit).WithSenderKey(key) + require.Equal(t, key, u.key) +} + +func TestValidationUnit_WithEACLTable(t *testing.T) { + eACL := NewTableForContainer(cidtest.ID(), []Record{ + ConstructRecord(Action(rand.Uint32()), Operation(rand.Uint32()), []Target{ + NewTargetByRole(Role(rand.Uint32())), + }), + }) + u := new(ValidationUnit).WithEACLTable(&eACL) + require.Equal(t, eACL, *u.table) +} diff --git a/eacl/validator_test.go b/eacl/validator_test.go index c6761ec2..6be17dbf 100644 --- a/eacl/validator_test.go +++ b/eacl/validator_test.go @@ -35,15 +35,15 @@ func TestFilterMatch(t *testing.T) { t.Run("simple header match", func(t *testing.T) { tb := NewTable() - r := newRecord(ActionDeny, OperationUnknown, tgt) + r := newRecord(ActionDeny, OperationUnspecified, tgt) r.AddFilter(HeaderFromObject, MatchStringEqual, "a", "xxx") tb.AddRecord(r) - r = newRecord(ActionDeny, OperationUnknown, tgt) + r = newRecord(ActionDeny, OperationUnspecified, tgt) r.AddFilter(HeaderFromRequest, MatchStringNotEqual, "b", "yyy") tb.AddRecord(r) - tb.AddRecord(newRecord(ActionAllow, OperationUnknown, tgt)) + tb.AddRecord(newRecord(ActionAllow, OperationUnspecified, tgt)) v := NewValidator() vu := newValidationUnit(RoleOthers, nil, tb) @@ -68,11 +68,11 @@ func TestFilterMatch(t *testing.T) { t.Run("all filters must match", func(t *testing.T) { tb := NewTable() - r := newRecord(ActionDeny, OperationUnknown, tgt) + r := newRecord(ActionDeny, OperationUnspecified, tgt) r.AddFilter(HeaderFromObject, MatchStringEqual, "a", "xxx") r.AddFilter(HeaderFromRequest, MatchStringEqual, "b", "yyy") tb.AddRecord(r) - tb.AddRecord(newRecord(ActionAllow, OperationUnknown, tgt)) + tb.AddRecord(newRecord(ActionAllow, OperationUnspecified, tgt)) v := NewValidator() vu := newValidationUnit(RoleOthers, nil, tb) @@ -91,15 +91,15 @@ func TestFilterMatch(t *testing.T) { t.Run("filters with unknown type are skipped", func(t *testing.T) { tb := NewTable() - r := newRecord(ActionDeny, OperationUnknown, tgt) - r.AddFilter(HeaderTypeUnknown, MatchStringEqual, "a", "xxx") + r := newRecord(ActionDeny, OperationUnspecified, tgt) + r.AddFilter(HeaderTypeUnspecified, MatchStringEqual, "a", "xxx") tb.AddRecord(r) - r = newRecord(ActionDeny, OperationUnknown, tgt) + r = newRecord(ActionDeny, OperationUnspecified, tgt) r.AddFilter(0xFF, MatchStringEqual, "b", "yyy") tb.AddRecord(r) - tb.AddRecord(newRecord(ActionDeny, OperationUnknown, tgt)) + tb.AddRecord(newRecord(ActionDeny, OperationUnspecified, tgt)) v := NewValidator() vu := newValidationUnit(RoleOthers, nil, tb) @@ -118,10 +118,10 @@ func TestFilterMatch(t *testing.T) { t.Run("filters with match function are skipped", func(t *testing.T) { tb := NewTable() - r := newRecord(ActionAllow, OperationUnknown, tgt) + r := newRecord(ActionAllow, OperationUnspecified, tgt) r.AddFilter(HeaderFromObject, 0xFF, "a", "xxx") tb.AddRecord(r) - tb.AddRecord(newRecord(ActionDeny, OperationUnknown, tgt)) + tb.AddRecord(newRecord(ActionDeny, OperationUnspecified, tgt)) v := NewValidator() vu := newValidationUnit(RoleOthers, nil, tb) @@ -156,7 +156,7 @@ func TestOperationMatch(t *testing.T) { t.Run("unknown operation", func(t *testing.T) { tb := NewTable() - tb.AddRecord(newRecord(ActionDeny, OperationUnknown, tgt)) + tb.AddRecord(newRecord(ActionDeny, OperationUnspecified, tgt)) tb.AddRecord(newRecord(ActionAllow, OperationGet, tgt)) v := NewValidator() @@ -192,7 +192,7 @@ func TestTargetMatches(t *testing.T) { u = newValidationUnit(RoleUser, pubs[2], nil) require.False(t, targetMatches(u, r)) - u = newValidationUnit(RoleUnknown, pubs[1], nil) + u = newValidationUnit(RoleUnspecified, pubs[1], nil) require.True(t, targetMatches(u, r)) u = newValidationUnit(RoleOthers, pubs[2], nil) diff --git a/pool/pool_aio_test.go b/pool/pool_aio_test.go index 1bd03afe..b0e19287 100644 --- a/pool/pool_aio_test.go +++ b/pool/pool_aio_test.go @@ -763,7 +763,7 @@ func testGetEacl(ctx context.Context, t *testing.T, containerID cid.ID, table ea newTable, err := setter.ContainerEACL(ctx, containerID, prm) require.NoError(t, err) - require.True(t, eacl.EqualTables(table, newTable)) + require.Equal(t, table.Marshal(), newTable.Marshal()) } func isBucketCreated(ctx context.Context, c containerGetter, id cid.ID) error {