[#58] eacl: move package from neofs-api-go

Also, remove deprecated methods.

Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
This commit is contained in:
Evgenii Stratonikov 2021-11-08 17:29:54 +03:00 committed by Alex Vanin
parent f05ff3901f
commit 8d313dbd5d
11 changed files with 2025 additions and 0 deletions

396
eacl/enums.go Normal file
View file

@ -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
}

214
eacl/enums_test.go Normal file
View file

@ -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"},
})
}

176
eacl/filter.go Normal file
View file

@ -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
}

88
eacl/filter_test.go Normal file
View file

@ -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())
})
}

268
eacl/record.go Normal file
View file

@ -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
}

258
eacl/record_test.go Normal file
View file

@ -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())
}

201
eacl/table.go Normal file
View file

@ -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
}

137
eacl/table_test.go Normal file
View file

@ -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())
})
}

157
eacl/target.go Normal file
View file

@ -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
}

85
eacl/target_test.go Normal file
View file

@ -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())
})
}

45
eacl/test/generate.go Normal file
View file

@ -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
}