[#67] object/eacl: Implement eACL validator

Signed-off-by: Leonard Lyubich <leonard@nspcc.ru>
This commit is contained in:
Leonard Lyubich 2020-10-02 15:23:52 +03:00 committed by Alex Vanin
parent 44fcd2f212
commit 69a69cdbee
9 changed files with 833 additions and 0 deletions

View file

@ -0,0 +1,38 @@
package eacl
import (
"github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl"
"github.com/nspcc-dev/neofs-api-go/pkg/container"
"github.com/nspcc-dev/neofs-node/pkg/morph/client/container/wrapper"
"github.com/nspcc-dev/neofs-node/pkg/util/logger"
)
type morphStorage struct {
w *wrapper.Wrapper
}
func (s *morphStorage) GetEACL(cid *container.ID) (*eacl.Table, error) {
table, _, err := s.w.GetEACL(cid)
return table, err
}
func WithLogger(v *logger.Logger) Option {
return func(c *cfg) {
c.logger = v
}
}
func WithEACLStorage(v Storage) Option {
return func(c *cfg) {
c.storage = v
}
}
func WithMorphClient(v *wrapper.Wrapper) Option {
return func(c *cfg) {
c.storage = &morphStorage{
w: v,
}
}
}

View file

@ -0,0 +1,86 @@
package eacl
import (
"github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl"
"github.com/nspcc-dev/neofs-api-go/pkg/container"
)
// Storage is the interface that wraps
// basic methods of extended ACL table storage.
type Storage interface {
// GetEACL reads the table from the storage by identifier.
// It returns any error encountered.
//
// GetEACL must return exactly one non-nil value.
GetEACL(*container.ID) (*eacl.Table, error)
}
// Header is an interface of string key-value header.
type Header interface {
GetKey() string
GetValue() string
}
// TypedHeaderSource is the interface that wraps
// method for selecting typed headers by type.
type TypedHeaderSource interface {
// HeadersOfType returns the list of key-value headers
// of particular type.
//
// It returns any problem encountered through the boolean
// false value.
HeadersOfType(eacl.FilterHeaderType) ([]Header, bool)
}
// ValidationUnit represents unit of check for Validator.
type ValidationUnit struct {
cid *container.ID
role eacl.Role
op eacl.Operation
hdrSrc TypedHeaderSource
key []byte
}
func (u *ValidationUnit) WithContainerID(v *container.ID) *ValidationUnit {
if u != nil {
u.cid = v
}
return u
}
func (u *ValidationUnit) WithRole(v eacl.Role) *ValidationUnit {
if u != nil {
u.role = v
}
return u
}
func (u *ValidationUnit) WithOperation(v eacl.Operation) *ValidationUnit {
if u != nil {
u.op = v
}
return u
}
func (u *ValidationUnit) WithHeaderSource(v TypedHeaderSource) *ValidationUnit {
if u != nil {
u.hdrSrc = v
}
return u
}
func (u *ValidationUnit) WithSenderKey(v []byte) *ValidationUnit {
if u != nil {
u.key = v
}
return u
}

View file

@ -0,0 +1,175 @@
package v2
import (
"crypto/rand"
"crypto/sha256"
"testing"
"github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl"
"github.com/nspcc-dev/neofs-api-go/pkg/container"
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object"
"github.com/nspcc-dev/neofs-api-go/v2/session"
crypto "github.com/nspcc-dev/neofs-crypto"
"github.com/nspcc-dev/neofs-node/pkg/core/object"
eacl2 "github.com/nspcc-dev/neofs-node/pkg/services/object/acl/eacl"
"github.com/nspcc-dev/neofs-node/pkg/util/test"
"github.com/stretchr/testify/require"
)
type testLocalStorage struct {
t *testing.T
expAddr *objectSDK.Address
obj *object.Object
}
type testEACLStorage struct {
t *testing.T
expCID *container.ID
table *eacl.Table
}
func (s *testEACLStorage) GetEACL(id *container.ID) (*eacl.Table, error) {
require.True(s.t, s.expCID.Equal(id))
return s.table, nil
}
func (s *testLocalStorage) Head(addr *objectSDK.Address) (*object.Object, error) {
require.True(s.t, addr.GetContainerID().Equal(addr.GetContainerID()) && addr.GetObjectID().Equal(addr.GetObjectID()))
return s.obj, nil
}
func testID(t *testing.T) *objectSDK.ID {
cs := [sha256.Size]byte{}
_, err := rand.Read(cs[:])
require.NoError(t, err)
id := objectSDK.NewID()
id.SetSHA256(cs)
return id
}
func testCID(t *testing.T) *container.ID {
cs := [sha256.Size]byte{}
_, err := rand.Read(cs[:])
require.NoError(t, err)
id := container.NewID()
id.SetSHA256(cs)
return id
}
func testAddress(t *testing.T) *objectSDK.Address {
addr := objectSDK.NewAddress()
addr.SetObjectID(testID(t))
addr.SetContainerID(testCID(t))
return addr
}
func testXHeaders(strs ...string) []*session.XHeader {
res := make([]*session.XHeader, 0, len(strs)/2)
for i := 0; i < len(strs); i += 2 {
x := new(session.XHeader)
x.SetKey(strs[i])
x.SetValue(strs[i+1])
res = append(res, x)
}
return res
}
func TestHeadRequest(t *testing.T) {
req := new(objectV2.HeadRequest)
meta := new(session.RequestMetaHeader)
req.SetMetaHeader(meta)
body := new(objectV2.HeadRequestBody)
req.SetBody(body)
addr := testAddress(t)
body.SetAddress(addr.ToV2())
xKey := "x-key"
xVal := "x-val"
xHdrs := testXHeaders(
xKey, xVal,
)
meta.SetXHeaders(xHdrs)
obj := object.NewRaw()
attrKey := "attr_key"
attrVal := "attr_val"
attr := objectSDK.NewAttribute()
attr.SetKey(attrKey)
attr.SetValue(attrVal)
obj.SetAttributes(attr)
table := new(eacl.Table)
senderKey := test.DecodeKey(-1).PublicKey
r := new(eacl.Record)
r.SetOperation(eacl.OperationHead)
r.SetAction(eacl.ActionDeny)
r.AddFilter(eacl.HeaderFromObject, eacl.MatchStringEqual, attrKey, attrVal)
r.AddFilter(eacl.HeaderFromRequest, eacl.MatchStringEqual, xKey, xVal)
r.AddTarget(eacl.RoleUnknown, senderKey)
table.AddRecord(r)
lStorage := &testLocalStorage{
t: t,
expAddr: addr,
obj: obj.Object(),
}
cid := addr.GetContainerID()
unit := new(eacl2.ValidationUnit).
WithContainerID(cid).
WithOperation(eacl.OperationHead).
WithSenderKey(crypto.MarshalPublicKey(&senderKey)).
WithHeaderSource(
NewMessageHeaderSource(
WithObjectStorage(lStorage),
WithServiceRequest(req),
),
)
eStorage := &testEACLStorage{
t: t,
expCID: cid,
table: table,
}
validator := eacl2.NewValidator(
eacl2.WithEACLStorage(eStorage),
)
require.Equal(t, eacl.ActionDeny, validator.CalculateAction(unit))
meta.SetXHeaders(nil)
require.Equal(t, eacl.ActionAllow, validator.CalculateAction(unit))
meta.SetXHeaders(xHdrs)
obj.SetAttributes(nil)
require.Equal(t, eacl.ActionAllow, validator.CalculateAction(unit))
}

View file

@ -0,0 +1,150 @@
package v2
import (
"fmt"
eaclSDK "github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl"
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
objectV2 "github.com/nspcc-dev/neofs-api-go/v2/object"
"github.com/nspcc-dev/neofs-api-go/v2/refs"
"github.com/nspcc-dev/neofs-api-go/v2/session"
"github.com/nspcc-dev/neofs-node/pkg/core/object"
"github.com/nspcc-dev/neofs-node/pkg/services/object/acl/eacl"
)
type Option func(*cfg)
type cfg struct {
storage ObjectStorage
msg xHeaderSource
}
type ObjectStorage interface {
Head(*objectSDK.Address) (*object.Object, error)
}
type Request interface {
GetMetaHeader() *session.RequestMetaHeader
}
type Response interface {
GetMetaHeader() *session.ResponseMetaHeader
}
type headerSource struct {
*cfg
}
func defaultCfg() *cfg {
return &cfg{
storage: new(localStorage),
}
}
func NewMessageHeaderSource(opts ...Option) eacl.TypedHeaderSource {
cfg := defaultCfg()
for i := range opts {
opts[i](cfg)
}
return &headerSource{
cfg: cfg,
}
}
func (h *headerSource) HeadersOfType(typ eaclSDK.FilterHeaderType) ([]eacl.Header, bool) {
switch typ {
default:
return nil, true
case eaclSDK.HeaderFromRequest:
return requestHeaders(h.msg), true
case eaclSDK.HeaderFromObject:
return h.objectHeaders()
}
}
func requestHeaders(msg xHeaderSource) []eacl.Header {
xHdrs := msg.GetXHeaders()
res := make([]eacl.Header, 0, len(xHdrs))
for i := range xHdrs {
res = append(res, xHdrs[i])
}
return res
}
func (h *headerSource) objectHeaders() ([]eacl.Header, bool) {
switch m := h.msg.(type) {
default:
panic(fmt.Sprintf("unexpected message type %T", h.msg))
case *requestXHeaderSource:
switch req := m.req.(type) {
case *objectV2.GetRequest:
return h.localObjectHeaders(req.GetBody().GetAddress())
case *objectV2.DeleteRequest:
return h.localObjectHeaders(req.GetBody().GetAddress())
case *objectV2.HeadRequest:
return h.localObjectHeaders(req.GetBody().GetAddress())
case *objectV2.GetRangeRequest:
return h.localObjectHeaders(req.GetBody().GetAddress())
case *objectV2.GetRangeHashRequest:
return h.localObjectHeaders(req.GetBody().GetAddress())
case *objectV2.PutRequest:
if v, ok := req.GetBody().GetObjectPart().(*objectV2.PutObjectPartInit); ok {
oV2 := new(objectV2.Object)
oV2.SetObjectID(v.GetObjectID())
oV2.SetHeader(v.GetHeader())
return headersFromObject(object.NewFromV2(oV2)), true
}
}
case *responseXHeaderSource:
switch resp := m.resp.(type) {
case *objectV2.GetResponse:
if v, ok := resp.GetBody().GetObjectPart().(*objectV2.GetObjectPartInit); ok {
oV2 := new(objectV2.Object)
oV2.SetObjectID(v.GetObjectID())
oV2.SetHeader(v.GetHeader())
return headersFromObject(object.NewFromV2(oV2)), true
}
case *objectV2.HeadResponse:
oV2 := new(objectV2.Object)
var hdr *objectV2.Header
switch v := resp.GetBody().GetHeaderPart().(type) {
case *objectV2.GetHeaderPartShort:
hdr = new(objectV2.Header)
h := v.GetShortHeader()
hdr.SetVersion(h.GetVersion())
hdr.SetCreationEpoch(h.GetCreationEpoch())
hdr.SetOwnerID(h.GetOwnerID())
hdr.SetObjectType(h.GetObjectType())
hdr.SetPayloadLength(h.GetPayloadLength())
case *objectV2.GetHeaderPartFull:
hdr = v.GetHeaderWithSignature().GetHeader()
}
oV2.SetHeader(hdr)
return headersFromObject(object.NewFromV2(oV2)), true
}
}
return nil, true
}
func (h *headerSource) localObjectHeaders(addr *refs.Address) ([]eacl.Header, bool) {
obj, err := h.storage.Head(objectSDK.NewAddressFromV2(addr))
if err == nil {
return headersFromObject(obj), true
}
return nil, false
}

View file

@ -0,0 +1,26 @@
package v2
import (
"io"
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-node/pkg/core/object"
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/localstore"
)
type localStorage struct {
ls *localstore.Storage
}
func (s *localStorage) Head(addr *objectSDK.Address) (*object.Object, error) {
if s.ls == nil {
return nil, io.ErrUnexpectedEOF
}
meta, err := s.ls.Head(addr)
if err != nil {
return nil, err
}
return meta.Head(), nil
}

View file

@ -0,0 +1,88 @@
package v2
import (
"encoding/hex"
"strconv"
"github.com/nspcc-dev/neofs-api-go/pkg/container"
objectSDK "github.com/nspcc-dev/neofs-api-go/pkg/object"
"github.com/nspcc-dev/neofs-api-go/pkg/owner"
"github.com/nspcc-dev/neofs-node/pkg/core/object"
"github.com/nspcc-dev/neofs-node/pkg/services/object/acl/eacl"
)
type sysObjHdr struct {
k, v string
}
func (s *sysObjHdr) GetKey() string {
return s.k
}
func (s *sysObjHdr) GetValue() string {
return s.v
}
// TODO: replace value conversions to neofs-api-go
func idValue(id *objectSDK.ID) string {
return hex.EncodeToString(id.ToV2().GetValue())
}
func cidValue(id *container.ID) string {
return hex.EncodeToString(id.ToV2().GetValue())
}
func ownerIDValue(id *owner.ID) string {
return hex.EncodeToString(id.ToV2().GetValue())
}
func u64Value(v uint64) string {
return strconv.FormatUint(v, 10)
}
func headersFromObject(obj *object.Object) []eacl.Header {
// TODO: optimize allocs
res := make([]eacl.Header, 0)
for ; obj != nil; obj = obj.GetParent() {
res = append(res,
// object ID
&sysObjHdr{
k: objectSDK.HdrSysNameID,
v: idValue(obj.GetID()),
},
// container ID
&sysObjHdr{
k: objectSDK.HdrSysNameCID,
v: cidValue(obj.GetContainerID()),
},
// owner ID
&sysObjHdr{
k: objectSDK.HdrSysNameOwnerID,
v: ownerIDValue(obj.GetOwnerID()),
},
// creation epoch
&sysObjHdr{
k: objectSDK.HdrSysNameCreatedEpoch,
v: u64Value(obj.GetCreationEpoch()),
},
// payload size
&sysObjHdr{
k: objectSDK.HdrSysNamePayloadLength,
v: u64Value(obj.GetPayloadSize()),
},
)
attrs := obj.GetAttributes()
hs := make([]eacl.Header, 0, len(attrs))
for i := range attrs {
hs = append(hs, attrs[i])
}
res = append(res, hs...)
}
return res
}

View file

@ -0,0 +1,35 @@
package v2
import (
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/localstore"
)
func WithObjectStorage(v ObjectStorage) Option {
return func(c *cfg) {
c.storage = v
}
}
func WithLocalObjectStorage(v *localstore.Storage) Option {
return func(c *cfg) {
c.storage = &localStorage{
ls: v,
}
}
}
func WithServiceRequest(v Request) Option {
return func(c *cfg) {
c.msg = &requestXHeaderSource{
req: v,
}
}
}
func WithServiceResponse(v Response) Option {
return func(c *cfg) {
c.msg = &responseXHeaderSource{
resp: v,
}
}
}

View file

@ -0,0 +1,63 @@
package v2
import (
"github.com/nspcc-dev/neofs-api-go/v2/session"
)
type xHeaderSource interface {
GetXHeaders() []*session.XHeader
}
type requestXHeaderSource struct {
req Request
}
type responseXHeaderSource struct {
resp Response
}
func (s *requestXHeaderSource) GetXHeaders() []*session.XHeader {
ln := 0
xHdrs := make([][]*session.XHeader, 0)
for meta := s.req.GetMetaHeader(); meta != nil; meta = meta.GetOrigin() {
x := meta.GetXHeaders()
ln += len(x)
xHdrs = append(xHdrs, x)
}
res := make([]*session.XHeader, 0, ln)
for i := range xHdrs {
for j := range xHdrs[i] {
res = append(res, xHdrs[i][j])
}
}
return res
}
func (s *responseXHeaderSource) GetXHeaders() []*session.XHeader {
ln := 0
xHdrs := make([][]*session.XHeader, 0)
for meta := s.resp.GetMetaHeader(); meta != nil; meta = meta.GetOrigin() {
x := meta.GetXHeaders()
ln += len(x)
xHdrs = append(xHdrs, x)
}
res := make([]*session.XHeader, 0, ln)
for i := range xHdrs {
for j := range xHdrs[i] {
res = append(res, xHdrs[i][j])
}
}
return res
}

View file

@ -0,0 +1,172 @@
package eacl
import (
"bytes"
"github.com/nspcc-dev/neofs-api-go/pkg/acl/eacl"
crypto "github.com/nspcc-dev/neofs-crypto"
"github.com/nspcc-dev/neofs-node/pkg/util/logger"
"go.uber.org/zap"
)
// Validator is a tool that calculates
// the action on a request according
// to the extended ACL rule table.
type Validator struct {
*cfg
}
// Option represents Validator option.
type Option func(*cfg)
type cfg struct {
logger *logger.Logger
storage Storage
}
func defaultCfg() *cfg {
return &cfg{
logger: zap.L(),
}
}
// NewValidator creates and initializes a new Validator using options.
func NewValidator(opts ...Option) *Validator {
cfg := defaultCfg()
for i := range opts {
opts[i](cfg)
}
return &Validator{
cfg: cfg,
}
}
// CalculateAction calculates action on the request according
// to its information represented in ValidationUnit.
//
// The action is calculated according to the application of
// eACL table of rules to the request.
//
// If the eACL table is not available at the time of the call,
// eacl.ActionUnknown is returned.
//
// If no matching table entry is found, ActionAllow is returned.
func (v *Validator) CalculateAction(unit *ValidationUnit) eacl.Action {
// get eACL table by container ID
table, err := v.storage.GetEACL(unit.cid)
if err != nil {
v.logger.Error("could not get eACL table",
zap.String("error", err.Error()),
)
return eacl.ActionUnknown
}
return tableAction(unit, table)
}
// calculates action on the request based on the eACL rules.
func tableAction(unit *ValidationUnit, table *eacl.Table) eacl.Action {
for _, record := range table.Records() {
// check type of operation
if record.Operation() != unit.op {
continue
}
// check target
if !targetMatches(unit, record) {
continue
}
// check headers
switch val := matchFilters(unit.hdrSrc, record.Filters()); {
case val < 0:
// headers of some type could not be composed => allow
return eacl.ActionAllow
case val == 0:
return record.Action()
}
}
return eacl.ActionAllow
}
// returns:
// - positive value if no matching header is found for at least one filter;
// - zero if at least one suitable header is found for all filters;
// - negative value if the headers of at least one filter cannot be obtained.
func matchFilters(hdrSrc TypedHeaderSource, filters []eacl.Filter) int {
matched := 0
for _, filter := range filters {
headers, ok := hdrSrc.HeadersOfType(filter.From())
if !ok {
return -1
}
// get headers of filtering type
for _, header := range headers {
// prevent NPE
if header == nil {
continue
}
// check header name
if header.GetKey() != filter.Name() {
continue
}
// get match function
matchFn, ok := mMatchFns[filter.Matcher()]
if !ok {
continue
}
// check match
if !matchFn(header, filter) {
continue
}
// increment match counter
matched++
break
}
}
return len(filters) - matched
}
// returns true if one of ExtendedACLTarget has
// suitable target OR suitable public key.
func targetMatches(unit *ValidationUnit, record eacl.Record) bool {
for _, target := range record.Targets() {
// check public key match
for _, key := range target.Keys() {
if bytes.Equal(crypto.MarshalPublicKey(&key), unit.key) {
return true
}
}
// check target group match
if unit.role == target.Role() {
return true
}
}
return false
}
// Maps match type to corresponding function.
var mMatchFns = map[eacl.Match]func(Header, eacl.Filter) bool{
eacl.MatchStringEqual: func(header Header, filter eacl.Filter) bool {
return header.GetValue() == filter.Value()
},
eacl.MatchStringNotEqual: func(header Header, filter eacl.Filter) bool {
return header.GetValue() != filter.Value()
},
}