diff --git a/eacl/enums.go b/eacl/enums.go new file mode 100644 index 00000000..2e8fc1f2 --- /dev/null +++ b/eacl/enums.go @@ -0,0 +1,396 @@ +package eacl + +import ( + v2acl "github.com/nspcc-dev/neofs-api-go/v2/acl" +) + +// Action taken if EACL record matched request. +// Action is compatible with v2 acl.Action enum. +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 +) + +// Operation is a object service method to match request. +// Operation is compatible with v2 acl.Operation enum. +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 +) + +// Role is a group of request senders to match request. +// Role is compatible with v2 acl.Role enum. +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 +) + +// Match is binary operation on filer name and value to check if request is matched. +// Match is compatible with v2 acl.MatchType enum. +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 +) + +// FilterHeaderType indicates source of headers to make matches. +// FilterHeaderType is compatible with v2 acl.HeaderType enum. +type FilterHeaderType uint32 + +const ( + // HeaderTypeUnknown is a FilterHeaderType value used to mark header type as undefined. + HeaderTypeUnknown FilterHeaderType = iota + + // HeaderFromRequest is a FilterHeaderType for request X-Header. + HeaderFromRequest + + // HeaderFromObject is a FilterHeaderType for object header. + HeaderFromObject + + // HeaderFromService is a FilterHeaderType for service header. + HeaderFromService +) + +// ToV2 converts Action to v2 Action enum value. +func (a Action) ToV2() v2acl.Action { + switch a { + case ActionAllow: + return v2acl.ActionAllow + case ActionDeny: + return v2acl.ActionDeny + default: + return v2acl.ActionUnknown + } +} + +// 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 + default: + a = ActionUnknown + } + + return a +} + +// String returns string representation of Action. +// +// String mapping: +// * ActionAllow: ALLOW; +// * ActionDeny: DENY; +// * ActionUnknown, default: ACTION_UNSPECIFIED. +func (a Action) String() string { + return a.ToV2().String() +} + +// FromString parses Action from a string representation. +// It is a reverse action to String(). +// +// Returns true if s was parsed successfully. +func (a *Action) FromString(s string) bool { + var g v2acl.Action + + ok := g.FromString(s) + + if ok { + *a = ActionFromV2(g) + } + + return ok +} + +// ToV2 converts Operation to v2 Operation enum value. +func (o Operation) ToV2() v2acl.Operation { + switch o { + case OperationGet: + return v2acl.OperationGet + case OperationHead: + return v2acl.OperationHead + case OperationPut: + return v2acl.OperationPut + case OperationDelete: + return v2acl.OperationDelete + case OperationSearch: + return v2acl.OperationSearch + case OperationRange: + return v2acl.OperationRange + case OperationRangeHash: + return v2acl.OperationRangeHash + default: + return v2acl.OperationUnknown + } +} + +// 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 + default: + o = OperationUnknown + } + + return o +} + +// String returns string representation of Operation. +// +// String mapping: +// * OperationGet: GET; +// * OperationHead: HEAD; +// * OperationPut: PUT; +// * OperationDelete: DELETE; +// * OperationSearch: SEARCH; +// * OperationRange: GETRANGE; +// * OperationRangeHash: GETRANGEHASH; +// * OperationUnknown, default: OPERATION_UNSPECIFIED. +func (o Operation) String() string { + return o.ToV2().String() +} + +// FromString parses Operation from a string representation. +// It is a reverse action to String(). +// +// Returns true if s was parsed successfully. +func (o *Operation) FromString(s string) bool { + var g v2acl.Operation + + ok := g.FromString(s) + + if ok { + *o = OperationFromV2(g) + } + + return ok +} + +// ToV2 converts Role to v2 Role enum value. +func (r Role) ToV2() v2acl.Role { + switch r { + case RoleUser: + return v2acl.RoleUser + case RoleSystem: + return v2acl.RoleSystem + case RoleOthers: + return v2acl.RoleOthers + default: + return v2acl.RoleUnknown + } +} + +// 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 + default: + r = RoleUnknown + } + + return r +} + +// String returns string representation of Role. +// +// String mapping: +// * RoleUser: USER; +// * RoleSystem: SYSTEM; +// * RoleOthers: OTHERS; +// * RoleUnknown, default: ROLE_UNKNOWN. +func (r Role) String() string { + return r.ToV2().String() +} + +// FromString parses Role from a string representation. +// It is a reverse action to String(). +// +// Returns true if s was parsed successfully. +func (r *Role) FromString(s string) bool { + var g v2acl.Role + + ok := g.FromString(s) + + if ok { + *r = RoleFromV2(g) + } + + return ok +} + +// ToV2 converts Match to v2 MatchType enum value. +func (m Match) ToV2() v2acl.MatchType { + switch m { + case MatchStringEqual: + return v2acl.MatchTypeStringEqual + case MatchStringNotEqual: + return v2acl.MatchTypeStringNotEqual + default: + return v2acl.MatchTypeUnknown + } +} + +// MatchFromV2 converts v2 MatchType enum value to Match. +func MatchFromV2(match v2acl.MatchType) (m Match) { + switch match { + case v2acl.MatchTypeStringEqual: + m = MatchStringEqual + case v2acl.MatchTypeStringNotEqual: + m = MatchStringNotEqual + default: + m = MatchUnknown + } + + return m +} + +// String returns string representation of Match. +// +// String mapping: +// * MatchStringEqual: STRING_EQUAL; +// * MatchStringNotEqual: STRING_NOT_EQUAL; +// * MatchUnknown, default: MATCH_TYPE_UNSPECIFIED. +func (m Match) String() string { + return m.ToV2().String() +} + +// FromString parses Match from a string representation. +// It is a reverse action to String(). +// +// Returns true if s was parsed successfully. +func (m *Match) FromString(s string) bool { + var g v2acl.MatchType + + ok := g.FromString(s) + + if ok { + *m = MatchFromV2(g) + } + + return ok +} + +// ToV2 converts FilterHeaderType to v2 HeaderType enum value. +func (h FilterHeaderType) ToV2() v2acl.HeaderType { + switch h { + case HeaderFromRequest: + return v2acl.HeaderTypeRequest + case HeaderFromObject: + return v2acl.HeaderTypeObject + case HeaderFromService: + return v2acl.HeaderTypeService + default: + return v2acl.HeaderTypeUnknown + } +} + +// 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 + default: + h = HeaderTypeUnknown + } + + return h +} + +// String returns string representation of FilterHeaderType. +// +// String mapping: +// * HeaderFromRequest: REQUEST; +// * HeaderFromObject: OBJECT; +// * HeaderTypeUnknown, default: HEADER_UNSPECIFIED. +func (h FilterHeaderType) String() string { + return h.ToV2().String() +} + +// FromString parses FilterHeaderType from a string representation. +// It is a reverse action to String(). +// +// Returns true if s was parsed successfully. +func (h *FilterHeaderType) FromString(s string) bool { + var g v2acl.HeaderType + + ok := g.FromString(s) + + if ok { + *h = FilterHeaderTypeFromV2(g) + } + + return ok +} diff --git a/eacl/enums_test.go b/eacl/enums_test.go new file mode 100644 index 00000000..44e64889 --- /dev/null +++ b/eacl/enums_test.go @@ -0,0 +1,214 @@ +package eacl_test + +import ( + "testing" + + v2acl "github.com/nspcc-dev/neofs-api-go/v2/acl" + "github.com/nspcc-dev/neofs-sdk-go/eacl" + "github.com/stretchr/testify/require" +) + +var ( + eqV2Actions = map[eacl.Action]v2acl.Action{ + eacl.ActionUnknown: 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, + } + + eqV2Roles = map[eacl.Role]v2acl.Role{ + eacl.RoleUnknown: 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.MatchStringEqual: v2acl.MatchTypeStringEqual, + eacl.MatchStringNotEqual: v2acl.MatchTypeStringNotEqual, + } + + eqV2HeaderTypes = map[eacl.FilterHeaderType]v2acl.HeaderType{ + eacl.HeaderTypeUnknown: v2acl.HeaderTypeUnknown, + eacl.HeaderFromRequest: v2acl.HeaderTypeRequest, + eacl.HeaderFromObject: v2acl.HeaderTypeObject, + eacl.HeaderFromService: v2acl.HeaderTypeService, + } +) + +func TestAction(t *testing.T) { + t.Run("known actions", func(t *testing.T) { + for i := eacl.ActionUnknown; 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) + }) +} + +func TestOperation(t *testing.T) { + t.Run("known operations", func(t *testing.T) { + for i := eacl.OperationUnknown; 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) + }) +} + +func TestRole(t *testing.T) { + t.Run("known roles", func(t *testing.T) { + for i := eacl.RoleUnknown; 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) + }) +} + +func TestMatch(t *testing.T) { + t.Run("known matches", func(t *testing.T) { + for i := eacl.MatchUnknown; 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.MatchStringNotEqual + 1).ToV2(), v2acl.MatchTypeUnknown) + require.Equal(t, eacl.MatchFromV2(v2acl.MatchTypeStringNotEqual+1), eacl.MatchUnknown) + }) +} + +func TestFilterHeaderType(t *testing.T) { + t.Run("known header types", func(t *testing.T) { + for i := eacl.HeaderTypeUnknown; 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) + }) +} + +type enumIface interface { + FromString(string) bool + String() string +} + +type enumStringItem struct { + val enumIface + str string +} + +func testEnumStrings(t *testing.T, e enumIface, items []enumStringItem) { + for _, item := range items { + require.Equal(t, item.str, item.val.String()) + + s := item.val.String() + + require.True(t, e.FromString(s), s) + + require.EqualValues(t, item.val, e, item.val) + } + + // incorrect strings + for _, str := range []string{ + "some string", + "UNSPECIFIED", + } { + require.False(t, e.FromString(str)) + } +} + +func TestAction_String(t *testing.T) { + toPtr := func(v eacl.Action) *eacl.Action { + return &v + } + + 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"}, + }) +} + +func TestRole_String(t *testing.T) { + toPtr := func(v eacl.Role) *eacl.Role { + return &v + } + + testEnumStrings(t, new(eacl.Role), []enumStringItem{ + {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"}, + }) +} + +func TestOperation_String(t *testing.T) { + toPtr := func(v eacl.Operation) *eacl.Operation { + return &v + } + + testEnumStrings(t, new(eacl.Operation), []enumStringItem{ + {val: toPtr(eacl.OperationGet), str: "GET"}, + {val: toPtr(eacl.OperationPut), str: "PUT"}, + {val: toPtr(eacl.OperationHead), str: "HEAD"}, + {val: toPtr(eacl.OperationDelete), str: "DELETE"}, + {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"}, + }) +} + +func TestMatch_String(t *testing.T) { + toPtr := func(v eacl.Match) *eacl.Match { + return &v + } + + 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"}, + }) +} + +func TestFilterHeaderType_String(t *testing.T) { + toPtr := func(v eacl.FilterHeaderType) *eacl.FilterHeaderType { + return &v + } + + 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"}, + }) +} diff --git a/eacl/filter.go b/eacl/filter.go new file mode 100644 index 00000000..ab5cb420 --- /dev/null +++ b/eacl/filter.go @@ -0,0 +1,176 @@ +package eacl + +import ( + "fmt" + "strconv" + + v2acl "github.com/nspcc-dev/neofs-api-go/v2/acl" +) + +// Filter defines check conditions if request header is matched or not. Matched +// header means that request should be processed according to EACL action. +// +// Filter is compatible with v2 acl.EACLRecord.Filter message. +type Filter struct { + from FilterHeaderType + matcher Match + key filterKey + value fmt.Stringer +} + +type staticStringer string + +type u64Stringer uint64 + +type filterKey struct { + typ filterKeyType + + str string +} + +// enumeration of reserved filter keys. +type filterKeyType int + +const ( + _ filterKeyType = iota + fKeyObjVersion + fKeyObjID + fKeyObjContainerID + fKeyObjOwnerID + fKeyObjCreationEpoch + fKeyObjPayloadLength + fKeyObjPayloadHash + fKeyObjType + fKeyObjHomomorphicHash +) + +func (s staticStringer) String() string { + return string(s) +} + +func (u u64Stringer) String() string { + return strconv.FormatUint(uint64(u), 10) +} + +// Value returns filtered string value. +func (f Filter) Value() string { + return f.value.String() +} + +// Matcher returns filter Match type. +func (f Filter) Matcher() Match { + return f.matcher +} + +// Key returns key to the filtered header. +func (f Filter) Key() string { + return f.key.String() +} + +// From returns FilterHeaderType that defined which header will be filtered. +func (f Filter) From() FilterHeaderType { + return f.from +} + +// ToV2 converts Filter to v2 acl.EACLRecord.Filter message. +// +// Nil Filter converts to nil. +func (f *Filter) ToV2() *v2acl.HeaderFilter { + if f == nil { + return nil + } + + filter := new(v2acl.HeaderFilter) + filter.SetValue(f.value.String()) + filter.SetKey(f.key.String()) + filter.SetMatchType(f.matcher.ToV2()) + filter.SetHeaderType(f.from.ToV2()) + + return filter +} + +func (k filterKey) String() string { + switch k.typ { + default: + return k.str + case fKeyObjVersion: + return v2acl.FilterObjectVersion + case fKeyObjID: + return v2acl.FilterObjectID + case fKeyObjContainerID: + return v2acl.FilterObjectContainerID + case fKeyObjOwnerID: + return v2acl.FilterObjectOwnerID + case fKeyObjCreationEpoch: + return v2acl.FilterObjectCreationEpoch + case fKeyObjPayloadLength: + return v2acl.FilterObjectPayloadLength + case fKeyObjPayloadHash: + return v2acl.FilterObjectPayloadHash + case fKeyObjType: + return v2acl.FilterObjectType + case fKeyObjHomomorphicHash: + return v2acl.FilterObjectHomomorphicHash + } +} + +// NewFilter creates, initializes and returns blank Filter instance. +// +// Defaults: +// - header type: HeaderTypeUnknown; +// - matcher: MatchUnknown; +// - key: ""; +// - value: "". +func NewFilter() *Filter { + return NewFilterFromV2(new(v2acl.HeaderFilter)) +} + +// NewFilterFromV2 converts v2 acl.EACLRecord.Filter message to Filter. +func NewFilterFromV2(filter *v2acl.HeaderFilter) *Filter { + f := new(Filter) + + if filter == nil { + return f + } + + f.from = FilterHeaderTypeFromV2(filter.GetHeaderType()) + f.matcher = MatchFromV2(filter.GetMatchType()) + f.key.str = filter.GetKey() + f.value = staticStringer(filter.GetValue()) + + return f +} + +// Marshal marshals Filter into a protobuf binary form. +func (f *Filter) Marshal() ([]byte, error) { + return f.ToV2().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 { + return err + } + + *f = *NewFilterFromV2(fV2) + + return nil +} + +// MarshalJSON encodes Filter to protobuf JSON format. +func (f *Filter) MarshalJSON() ([]byte, error) { + return f.ToV2().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 { + return err + } + + *f = *NewFilterFromV2(fV2) + + return nil +} diff --git a/eacl/filter_test.go b/eacl/filter_test.go new file mode 100644 index 00000000..9a471d0f --- /dev/null +++ b/eacl/filter_test.go @@ -0,0 +1,88 @@ +package eacl + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/v2/acl" + v2acl "github.com/nspcc-dev/neofs-api-go/v2/acl" + "github.com/stretchr/testify/require" +) + +func newObjectFilter(match Match, key, val string) *Filter { + return &Filter{ + from: HeaderFromObject, + key: filterKey{ + str: key, + }, + matcher: match, + value: staticStringer(val), + } +} + +func TestFilter(t *testing.T) { + filter := newObjectFilter(MatchStringEqual, "some name", "200") + + 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()) + + newFilter := NewFilterFromV2(v2) + require.Equal(t, filter, newFilter) + + t.Run("from nil v2 filter", func(t *testing.T) { + require.Equal(t, new(Filter), NewFilterFromV2(nil)) + }) +} + +func TestFilterEncoding(t *testing.T) { + f := newObjectFilter(MatchStringEqual, "key", "value") + + t.Run("binary", func(t *testing.T) { + data, err := f.Marshal() + require.NoError(t, err) + + f2 := NewFilter() + require.NoError(t, f2.Unmarshal(data)) + + require.Equal(t, f, f2) + }) + + t.Run("json", func(t *testing.T) { + data, err := f.MarshalJSON() + require.NoError(t, err) + + d2 := NewFilter() + require.NoError(t, d2.UnmarshalJSON(data)) + + require.Equal(t, f, d2) + }) +} + +func TestFilter_ToV2(t *testing.T) { + t.Run("nil", func(t *testing.T) { + var x *Filter + + require.Nil(t, x.ToV2()) + }) + + t.Run("default values", func(t *testing.T) { + filter := NewFilter() + + // 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()) + + // convert to v2 message + filterV2 := filter.ToV2() + + require.Empty(t, filterV2.GetKey()) + require.Empty(t, filterV2.GetValue()) + require.Equal(t, acl.HeaderTypeUnknown, filterV2.GetHeaderType()) + require.Equal(t, acl.MatchTypeUnknown, filterV2.GetMatchType()) + }) +} diff --git a/eacl/record.go b/eacl/record.go new file mode 100644 index 00000000..05fd109c --- /dev/null +++ b/eacl/record.go @@ -0,0 +1,268 @@ +package eacl + +import ( + "crypto/ecdsa" + "fmt" + + v2acl "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/object" + "github.com/nspcc-dev/neofs-sdk-go/owner" + "github.com/nspcc-dev/neofs-sdk-go/version" +) + +// Record of the EACL rule, that defines EACL action, targets for this action, +// object service operation and filters for request headers. +// +// Record is compatible with v2 acl.EACLRecord message. +type Record struct { + action Action + operation Operation + filters []*Filter + targets []*Target +} + +// Targets returns list of target subjects to apply ACL rule to. +func (r Record) Targets() []*Target { + return r.targets +} + +// SetTargets sets list of target subjects to apply ACL rule to. +func (r *Record) SetTargets(targets ...*Target) { + r.targets = targets +} + +// Filters returns list of filters to match and see if rule is applicable. +func (r Record) Filters() []*Filter { + return r.filters +} + +// Operation returns NeoFS request verb to match. +func (r Record) Operation() Operation { + return r.operation +} + +// SetOperation sets NeoFS request verb to match. +func (r *Record) SetOperation(operation Operation) { + r.operation = operation +} + +// Action returns rule execution result. +func (r Record) Action() Action { + return r.action +} + +// SetAction sets rule execution result. +func (r *Record) SetAction(action Action) { + r.action = action +} + +// AddRecordTarget adds single Target to the Record. +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. +func AddFormedTarget(r *Record, role Role, keys ...ecdsa.PublicKey) { + t := NewTarget() + t.SetRole(role) + + SetTargetECDSAKeys(t, ecdsaKeysToPtrs(keys)...) + AddRecordTarget(r, t) +} + +func (r *Record) addFilter(from FilterHeaderType, m Match, keyTyp filterKeyType, key string, val fmt.Stringer) { + filter := &Filter{ + from: from, + key: filterKey{ + typ: keyTyp, + str: key, + }, + matcher: m, + value: val, + } + + r.filters = append(r.filters, filter) +} + +func (r *Record) addObjectFilter(m Match, keyTyp filterKeyType, key string, val fmt.Stringer) { + r.addFilter(HeaderFromObject, m, keyTyp, key, val) +} + +func (r *Record) addObjectReservedFilter(m Match, typ filterKeyType, val fmt.Stringer) { + r.addObjectFilter(m, typ, "", val) +} + +// AddFilter adds generic filter. +func (r *Record) AddFilter(from FilterHeaderType, matcher Match, name, value string) { + r.addFilter(from, matcher, 0, name, staticStringer(value)) +} + +// AddObjectAttributeFilter adds filter by object attribute. +func (r *Record) AddObjectAttributeFilter(m Match, key, value string) { + r.addObjectFilter(m, 0, key, staticStringer(value)) +} + +// AddObjectVersionFilter adds filter by object version. +func (r *Record) AddObjectVersionFilter(m Match, v *version.Version) { + r.addObjectReservedFilter(m, fKeyObjVersion, v) +} + +// AddObjectIDFilter adds filter by object ID. +func (r *Record) AddObjectIDFilter(m Match, id *object.ID) { + r.addObjectReservedFilter(m, fKeyObjID, id) +} + +// AddObjectContainerIDFilter adds filter by object container ID. +func (r *Record) AddObjectContainerIDFilter(m Match, id *cid.ID) { + r.addObjectReservedFilter(m, fKeyObjContainerID, id) +} + +// AddObjectOwnerIDFilter adds filter by object owner ID. +func (r *Record) AddObjectOwnerIDFilter(m Match, id *owner.ID) { + r.addObjectReservedFilter(m, fKeyObjOwnerID, id) +} + +// AddObjectCreationEpoch adds filter by object creation epoch. +func (r *Record) AddObjectCreationEpoch(m Match, epoch uint64) { + r.addObjectReservedFilter(m, fKeyObjCreationEpoch, u64Stringer(epoch)) +} + +// AddObjectPayloadLengthFilter adds filter by object payload length. +func (r *Record) AddObjectPayloadLengthFilter(m Match, size uint64) { + r.addObjectReservedFilter(m, fKeyObjPayloadLength, u64Stringer(size)) +} + +// AddObjectPayloadHashFilter adds filter by object payload hash value. +func (r *Record) AddObjectPayloadHashFilter(m Match, h *checksum.Checksum) { + r.addObjectReservedFilter(m, fKeyObjPayloadHash, h) +} + +// AddObjectTypeFilter adds filter by object type. +func (r *Record) AddObjectTypeFilter(m Match, t object.Type) { + r.addObjectReservedFilter(m, fKeyObjType, t) +} + +// AddObjectHomomorphicHashFilter adds filter by object payload homomorphic hash value. +func (r *Record) AddObjectHomomorphicHashFilter(m Match, h *checksum.Checksum) { + r.addObjectReservedFilter(m, fKeyObjHomomorphicHash, h) +} + +// ToV2 converts Record to v2 acl.EACLRecord message. +// +// Nil Record converts to nil. +func (r *Record) ToV2() *v2acl.Record { + if r == nil { + return nil + } + + v2 := new(v2acl.Record) + + if r.targets != nil { + targets := make([]*v2acl.Target, 0, len(r.targets)) + for _, target := range r.targets { + targets = append(targets, target.ToV2()) + } + + v2.SetTargets(targets) + } + + if r.filters != nil { + filters := make([]*v2acl.HeaderFilter, 0, len(r.filters)) + for _, filter := range r.filters { + filters = append(filters, filter.ToV2()) + } + + v2.SetFilters(filters) + } + + v2.SetAction(r.action.ToV2()) + v2.SetOperation(r.operation.ToV2()) + + return v2 +} + +// NewRecord creates and returns blank Record instance. +// +// Defaults: +// - action: ActionUnknown; +// - operation: OperationUnknown; +// - targets: nil, +// - filters: nil. +func NewRecord() *Record { + return new(Record) +} + +// CreateRecord creates, initializes with parameters and returns Record instance. +func CreateRecord(action Action, operation Operation) *Record { + r := NewRecord() + r.action = action + r.operation = operation + r.targets = []*Target{} + r.filters = []*Filter{} + + return r +} + +// NewRecordFromV2 converts v2 acl.EACLRecord message to Record. +func NewRecordFromV2(record *v2acl.Record) *Record { + r := NewRecord() + + if record == nil { + return r + } + + r.action = ActionFromV2(record.GetAction()) + r.operation = OperationFromV2(record.GetOperation()) + + v2targets := record.GetTargets() + v2filters := record.GetFilters() + + r.targets = make([]*Target, 0, len(v2targets)) + for i := range v2targets { + r.targets = append(r.targets, NewTargetFromV2(v2targets[i])) + } + + r.filters = make([]*Filter, 0, len(v2filters)) + for i := range v2filters { + r.filters = append(r.filters, NewFilterFromV2(v2filters[i])) + } + + return r +} + +// Marshal marshals Record into a protobuf binary form. +func (r *Record) Marshal() ([]byte, error) { + return r.ToV2().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 { + return err + } + + *r = *NewRecordFromV2(fV2) + + return nil +} + +// MarshalJSON encodes Record to protobuf JSON format. +func (r *Record) MarshalJSON() ([]byte, error) { + return r.ToV2().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 { + return err + } + + *r = *NewRecordFromV2(tV2) + + return nil +} diff --git a/eacl/record_test.go b/eacl/record_test.go new file mode 100644 index 00000000..dd33d50b --- /dev/null +++ b/eacl/record_test.go @@ -0,0 +1,258 @@ +package eacl + +import ( + "crypto/ecdsa" + "fmt" + "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" + objecttest "github.com/nspcc-dev/neofs-sdk-go/object/test" + ownertest "github.com/nspcc-dev/neofs-sdk-go/owner/test" + versiontest "github.com/nspcc-dev/neofs-sdk-go/version/test" + "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) { + items := []struct { + role Role + keys []ecdsa.PublicKey + }{ + { + role: RoleUnknown, + keys: []ecdsa.PublicKey{*randomPublicKey(t)}, + }, + { + role: RoleSystem, + keys: []ecdsa.PublicKey{}, + }, + } + + targets := make([]*Target, 0, len(items)) + + r := NewRecord() + + for _, item := range items { + tgt := NewTarget() + tgt.SetRole(item.role) + SetTargetECDSAKeys(tgt, ecdsaKeysToPtrs(item.keys)...) + + targets = append(targets, tgt) + + AddFormedTarget(r, item.role, item.keys...) + } + + tgts := r.Targets() + require.Len(t, tgts, len(targets)) + + for _, tgt := range targets { + require.Contains(t, tgts, tgt) + } +} + +func TestRecord_AddFilter(t *testing.T) { + filters := []*Filter{ + newObjectFilter(MatchStringEqual, "some name", "ContainerID"), + newObjectFilter(MatchStringNotEqual, "X-Header-Name", "X-Header-Value"), + } + + r := NewRecord() + for _, filter := range filters { + r.AddFilter(filter.From(), filter.Matcher(), filter.Key(), filter.Value()) + } + + require.Equal(t, filters, r.Filters()) +} + +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) { + data, err := r.Marshal() + require.NoError(t, err) + + r2 := NewRecord() + require.NoError(t, r2.Unmarshal(data)) + + require.Equal(t, r, r2) + }) + + t.Run("json", func(t *testing.T) { + data, err := r.MarshalJSON() + require.NoError(t, err) + + r2 := NewRecord() + require.NoError(t, r2.UnmarshalJSON(data)) + + require.Equal(t, r, r2) + }) +} + +func TestRecord_ToV2(t *testing.T) { + t.Run("nil", func(t *testing.T) { + var x *Record + + require.Nil(t, x.ToV2()) + }) + + t.Run("default values", func(t *testing.T) { + record := NewRecord() + + // 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()) + + // convert to v2 message + recordV2 := record.ToV2() + + 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 TestReservedRecords(t *testing.T) { + var ( + v = versiontest.Version() + oid = objecttest.ID() + cid = cidtest.GenerateID() + ownerid = ownertest.GenerateID() + 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.String(), + }, + { + f: func(r *Record) { r.AddObjectContainerIDFilter(MatchStringEqual, cid) }, + key: v2acl.FilterObjectContainerID, + value: cid.String(), + }, + { + f: func(r *Record) { r.AddObjectOwnerIDFilter(MatchStringEqual, ownerid) }, + key: v2acl.FilterObjectOwnerID, + value: ownerid.String(), + }, + { + 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.FromString("REGULAR")) + r.AddObjectTypeFilter(MatchStringEqual, *typ) + }, + key: v2acl.FilterObjectType, + value: "REGULAR", + }, + { + f: func(r *Record) { + require.True(t, typ.FromString("TOMBSTONE")) + r.AddObjectTypeFilter(MatchStringEqual, *typ) + }, + key: v2acl.FilterObjectType, + value: "TOMBSTONE", + }, + { + f: func(r *Record) { + require.True(t, typ.FromString("STORAGE_GROUP")) + r.AddObjectTypeFilter(MatchStringEqual, *typ) + }, + key: v2acl.FilterObjectType, + value: "STORAGE_GROUP", + }, + } + + 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 randomPublicKey(t *testing.T) *ecdsa.PublicKey { + p, err := keys.NewPrivateKey() + require.NoError(t, err) + + return (*ecdsa.PublicKey)(p.PublicKey()) +} diff --git a/eacl/table.go b/eacl/table.go new file mode 100644 index 00000000..d71095b1 --- /dev/null +++ b/eacl/table.go @@ -0,0 +1,201 @@ +package eacl + +import ( + "crypto/sha256" + + v2acl "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/session" + "github.com/nspcc-dev/neofs-sdk-go/signature" + "github.com/nspcc-dev/neofs-sdk-go/version" +) + +// Table is a group of EACL records for single container. +// +// Table is compatible with v2 acl.EACLTable message. +type Table struct { + version version.Version + cid *cid.ID + token *session.Token + sig *signature.Signature + records []*Record +} + +// CID returns identifier of the container that should use given access control rules. +func (t Table) CID() *cid.ID { + return t.cid +} + +// SetCID sets identifier of the container that should use given access control rules. +func (t *Table) SetCID(cid *cid.ID) { + t.cid = cid +} + +// Version returns version of eACL format. +func (t Table) Version() version.Version { + return t.version +} + +// SetVersion sets version of eACL format. +func (t *Table) SetVersion(version version.Version) { + t.version = version +} + +// Records returns list of extended ACL rules. +func (t Table) Records() []*Record { + return t.records +} + +// AddRecord adds single eACL rule. +func (t *Table) AddRecord(r *Record) { + if r != nil { + t.records = append(t.records, r) + } +} + +// SessionToken returns token of the session +// within which Table was set. +func (t Table) SessionToken() *session.Token { + return t.token +} + +// SetSessionToken sets token of the session +// within which Table was set. +func (t *Table) SetSessionToken(tok *session.Token) { + t.token = tok +} + +// Signature returns Table signature. +func (t Table) Signature() *signature.Signature { + return t.sig +} + +// SetSignature sets Table signature. +func (t *Table) SetSignature(sig *signature.Signature) { + t.sig = sig +} + +// ToV2 converts Table to v2 acl.EACLTable message. +// +// Nil Table converts to nil. +func (t *Table) ToV2() *v2acl.Table { + if t == nil { + return nil + } + + v2 := new(v2acl.Table) + + if t.cid != nil { + v2.SetContainerID(t.cid.ToV2()) + } + + if t.records != nil { + records := make([]*v2acl.Record, 0, len(t.records)) + for _, record := range t.records { + records = append(records, record.ToV2()) + } + + v2.SetRecords(records) + } + + v2.SetVersion(t.version.ToV2()) + + return v2 +} + +// NewTable creates, initializes and returns blank Table instance. +// +// Defaults: +// - version: version.Current(); +// - container ID: nil; +// - records: nil; +// - session token: nil; +// - signature: nil. +func NewTable() *Table { + t := new(Table) + t.SetVersion(*version.Current()) + + return t +} + +// CreateTable creates, initializes with parameters and returns Table instance. +func CreateTable(cid cid.ID) *Table { + t := NewTable() + t.SetCID(&cid) + + return t +} + +// NewTableFromV2 converts v2 acl.EACLTable message to Table. +func NewTableFromV2(table *v2acl.Table) *Table { + t := new(Table) + + if table == nil { + return t + } + + // set version + if v := table.GetVersion(); v != nil { + ver := version.Version{} + ver.SetMajor(v.GetMajor()) + ver.SetMinor(v.GetMinor()) + + t.SetVersion(ver) + } + + // set container id + if id := table.GetContainerID(); id != nil { + if t.cid == nil { + t.cid = new(cid.ID) + } + + var h [sha256.Size]byte + + copy(h[:], id.GetValue()) + t.cid.SetSHA256(h) + } + + // set eacl records + v2records := table.GetRecords() + t.records = make([]*Record, 0, len(v2records)) + + for i := range v2records { + t.records = append(t.records, NewRecordFromV2(v2records[i])) + } + + return t +} + +// Marshal marshals Table into a protobuf binary form. +func (t *Table) Marshal() ([]byte, error) { + return t.ToV2().StableMarshal(nil) +} + +// Unmarshal unmarshals protobuf binary representation of Table. +func (t *Table) Unmarshal(data []byte) error { + fV2 := new(v2acl.Table) + if err := fV2.Unmarshal(data); err != nil { + return err + } + + *t = *NewTableFromV2(fV2) + + return nil +} + +// MarshalJSON encodes Table to protobuf JSON format. +func (t *Table) MarshalJSON() ([]byte, error) { + return t.ToV2().MarshalJSON() +} + +// UnmarshalJSON decodes Table from protobuf JSON format. +func (t *Table) UnmarshalJSON(data []byte) error { + tV2 := new(v2acl.Table) + if err := tV2.UnmarshalJSON(data); err != nil { + return err + } + + *t = *NewTableFromV2(tV2) + + return nil +} diff --git a/eacl/table_test.go b/eacl/table_test.go new file mode 100644 index 00000000..3412da1e --- /dev/null +++ b/eacl/table_test.go @@ -0,0 +1,137 @@ +package eacl_test + +import ( + "crypto/sha256" + "testing" + + 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" + sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/test" + "github.com/nspcc-dev/neofs-sdk-go/signature" + "github.com/nspcc-dev/neofs-sdk-go/version" + "github.com/stretchr/testify/require" +) + +func TestTable(t *testing.T) { + var v version.Version + + sha := sha256.Sum256([]byte("container id")) + id := cidtest.GenerateIDWithChecksum(sha) + + 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, sha[:], v2.GetContainerID().GetValue()) + require.Len(t, v2.GetRecords(), 1) + + newTable := eacl.NewTableFromV2(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.GenerateID() + + table := eacl.CreateTable(*id) + require.Equal(t, id, table.CID()) + require.Equal(t, *version.Current(), table.Version()) + }) +} + +func TestTable_AddRecord(t *testing.T) { + records := []*eacl.Record{ + eacl.CreateRecord(eacl.ActionDeny, eacl.OperationDelete), + eacl.CreateRecord(eacl.ActionAllow, eacl.OperationPut), + } + + 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) { + data, err := tab.Marshal() + require.NoError(t, err) + + tab2 := eacl.NewTable() + require.NoError(t, tab2.Unmarshal(data)) + + // 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_SessionToken(t *testing.T) { + tok := sessiontest.Generate() + + table := eacl.NewTable() + table.SetSessionToken(tok) + + require.Equal(t, tok, table.SessionToken()) +} + +func TestTable_Signature(t *testing.T) { + sig := signature.New() + sig.SetKey([]byte{1, 2, 3}) + sig.SetSign([]byte{4, 5, 6}) + + table := eacl.NewTable() + table.SetSignature(sig) + + require.Equal(t, sig, table.Signature()) +} + +func TestTable_ToV2(t *testing.T) { + t.Run("nil", func(t *testing.T) { + var x *eacl.Table + + require.Nil(t, x.ToV2()) + }) + + 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.Nil(t, table.CID()) + require.Nil(t, table.SessionToken()) + require.Nil(t, table.Signature()) + + // convert to v2 message + tableV2 := table.ToV2() + + require.Equal(t, version.Current().ToV2(), tableV2.GetVersion()) + require.Nil(t, tableV2.GetRecords()) + require.Nil(t, tableV2.GetContainerID()) + }) +} diff --git a/eacl/target.go b/eacl/target.go new file mode 100644 index 00000000..249628b7 --- /dev/null +++ b/eacl/target.go @@ -0,0 +1,157 @@ +package eacl + +import ( + "crypto/ecdsa" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + v2acl "github.com/nspcc-dev/neofs-api-go/v2/acl" +) + +// Target is a group of request senders to match EACL. Defined by role enum +// and set of public keys. +// +// Target is compatible with v2 acl.EACLRecord.Target message. +type Target struct { + role Role + keys [][]byte +} + +func ecdsaKeysToPtrs(keys []ecdsa.PublicKey) []*ecdsa.PublicKey { + keysPtr := make([]*ecdsa.PublicKey, len(keys)) + + for i := range keys { + keysPtr[i] = &keys[i] + } + + return keysPtr +} + +// BinaryKeys returns list of public keys to identify +// target subject in a binary format. +func (t *Target) BinaryKeys() [][]byte { + return t.keys +} + +// SetBinaryKeys sets list of binary public keys to identify +// target subject. +func (t *Target) SetBinaryKeys(keys [][]byte) { + t.keys = keys +} + +// SetTargetECDSAKeys converts ECDSA public keys to a binary +// format and stores them in Target. +func SetTargetECDSAKeys(t *Target, pubs ...*ecdsa.PublicKey) { + binKeys := t.BinaryKeys() + ln := len(pubs) + + if cap(binKeys) >= ln { + binKeys = binKeys[:0] + } else { + binKeys = make([][]byte, 0, ln) + } + + for i := 0; i < ln; i++ { + binKeys = append(binKeys, (*keys.PublicKey)(pubs[i]).Bytes()) + } + + t.SetBinaryKeys(binKeys) +} + +// TargetECDSAKeys interprets binary public keys of Target +// as ECDSA public keys. If any key has a different format, +// the corresponding element will be nil. +func TargetECDSAKeys(t *Target) []*ecdsa.PublicKey { + binKeys := t.BinaryKeys() + ln := len(binKeys) + + pubs := make([]*ecdsa.PublicKey, ln) + + for i := 0; i < ln; i++ { + p := new(keys.PublicKey) + if p.DecodeBytes(binKeys[i]) == nil { + pubs[i] = (*ecdsa.PublicKey)(p) + } + } + + return pubs +} + +// SetRole sets target subject's role class. +func (t *Target) SetRole(r Role) { + t.role = r +} + +// Role returns target subject's role class. +func (t Target) Role() Role { + return t.role +} + +// ToV2 converts Target to v2 acl.EACLRecord.Target message. +// +// Nil Target converts to nil. +func (t *Target) ToV2() *v2acl.Target { + if t == nil { + return nil + } + + target := new(v2acl.Target) + target.SetRole(t.role.ToV2()) + target.SetKeys(t.keys) + + return target +} + +// NewTarget creates, initializes and returns blank Target instance. +// +// Defaults: +// - role: RoleUnknown; +// - keys: nil. +func NewTarget() *Target { + return NewTargetFromV2(new(v2acl.Target)) +} + +// NewTargetFromV2 converts v2 acl.EACLRecord.Target message to Target. +func NewTargetFromV2(target *v2acl.Target) *Target { + if target == nil { + return new(Target) + } + + return &Target{ + role: RoleFromV2(target.GetRole()), + keys: target.GetKeys(), + } +} + +// Marshal marshals Target into a protobuf binary form. +func (t *Target) Marshal() ([]byte, error) { + return t.ToV2().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 { + return err + } + + *t = *NewTargetFromV2(fV2) + + return nil +} + +// MarshalJSON encodes Target to protobuf JSON format. +func (t *Target) MarshalJSON() ([]byte, error) { + return t.ToV2().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 { + return err + } + + *t = *NewTargetFromV2(tV2) + + return nil +} diff --git a/eacl/target_test.go b/eacl/target_test.go new file mode 100644 index 00000000..2afd6f93 --- /dev/null +++ b/eacl/target_test.go @@ -0,0 +1,85 @@ +package eacl + +import ( + "crypto/ecdsa" + "testing" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neofs-api-go/v2/acl" + v2acl "github.com/nspcc-dev/neofs-api-go/v2/acl" + "github.com/stretchr/testify/require" +) + +func TestTarget(t *testing.T) { + pubs := []*ecdsa.PublicKey{ + randomPublicKey(t), + randomPublicKey(t), + } + + target := NewTarget() + target.SetRole(RoleSystem) + SetTargetECDSAKeys(target, pubs...) + + 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()) + } + + 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 TestTargetEncoding(t *testing.T) { + tar := NewTarget() + tar.SetRole(RoleSystem) + SetTargetECDSAKeys(tar, randomPublicKey(t)) + + t.Run("binary", func(t *testing.T) { + data, err := tar.Marshal() + require.NoError(t, err) + + tar2 := NewTarget() + require.NoError(t, tar2.Unmarshal(data)) + + require.Equal(t, tar, tar2) + }) + + t.Run("json", func(t *testing.T) { + data, err := tar.MarshalJSON() + require.NoError(t, err) + + tar2 := NewTarget() + require.NoError(t, tar2.UnmarshalJSON(data)) + + require.Equal(t, tar, tar2) + }) +} + +func TestTarget_ToV2(t *testing.T) { + t.Run("nil", func(t *testing.T) { + var x *Target + + require.Nil(t, x.ToV2()) + }) + + t.Run("default values", func(t *testing.T) { + target := NewTarget() + + // check initial values + require.Equal(t, RoleUnknown, target.Role()) + require.Nil(t, target.BinaryKeys()) + + // convert to v2 message + targetV2 := target.ToV2() + + require.Equal(t, acl.RoleUnknown, targetV2.GetRole()) + require.Nil(t, targetV2.GetKeys()) + }) +} diff --git a/eacl/test/generate.go b/eacl/test/generate.go new file mode 100644 index 00000000..8da310cd --- /dev/null +++ b/eacl/test/generate.go @@ -0,0 +1,45 @@ +package eacltest + +import ( + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" + "github.com/nspcc-dev/neofs-sdk-go/eacl" + ownertest "github.com/nspcc-dev/neofs-sdk-go/owner/test" + versiontest "github.com/nspcc-dev/neofs-sdk-go/version/test" +) + +// Target returns random eacl.Target. +func Target() *eacl.Target { + x := eacl.NewTarget() + + x.SetRole(eacl.RoleSystem) + x.SetBinaryKeys([][]byte{ + {1, 2, 3}, + {4, 5, 6}, + }) + + return x +} + +// 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.GenerateID()) + x.AddObjectOwnerIDFilter(eacl.MatchStringNotEqual, ownertest.GenerateID()) + + return x +} + +func Table() *eacl.Table { + x := eacl.NewTable() + + x.SetCID(cidtest.GenerateID()) + x.AddRecord(Record()) + x.AddRecord(Record()) + x.SetVersion(*versiontest.Version()) + + return x +}