From 69a69cdbee45e8b6998f0e9d3672e393ad3d5e29 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Fri, 2 Oct 2020 15:23:52 +0300 Subject: [PATCH] [#67] object/eacl: Implement eACL validator Signed-off-by: Leonard Lyubich --- pkg/services/object/acl/eacl/opts.go | 38 ++++ pkg/services/object/acl/eacl/types.go | 86 +++++++++ pkg/services/object/acl/eacl/v2/eacl_test.go | 175 ++++++++++++++++++ pkg/services/object/acl/eacl/v2/headers.go | 150 +++++++++++++++ pkg/services/object/acl/eacl/v2/localstore.go | 26 +++ pkg/services/object/acl/eacl/v2/object.go | 88 +++++++++ pkg/services/object/acl/eacl/v2/opts.go | 35 ++++ pkg/services/object/acl/eacl/v2/xheader.go | 63 +++++++ pkg/services/object/acl/eacl/validator.go | 172 +++++++++++++++++ 9 files changed, 833 insertions(+) create mode 100644 pkg/services/object/acl/eacl/opts.go create mode 100644 pkg/services/object/acl/eacl/types.go create mode 100644 pkg/services/object/acl/eacl/v2/eacl_test.go create mode 100644 pkg/services/object/acl/eacl/v2/headers.go create mode 100644 pkg/services/object/acl/eacl/v2/localstore.go create mode 100644 pkg/services/object/acl/eacl/v2/object.go create mode 100644 pkg/services/object/acl/eacl/v2/opts.go create mode 100644 pkg/services/object/acl/eacl/v2/xheader.go create mode 100644 pkg/services/object/acl/eacl/validator.go diff --git a/pkg/services/object/acl/eacl/opts.go b/pkg/services/object/acl/eacl/opts.go new file mode 100644 index 000000000..63b92bae6 --- /dev/null +++ b/pkg/services/object/acl/eacl/opts.go @@ -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, + } + } +} diff --git a/pkg/services/object/acl/eacl/types.go b/pkg/services/object/acl/eacl/types.go new file mode 100644 index 000000000..709572475 --- /dev/null +++ b/pkg/services/object/acl/eacl/types.go @@ -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 +} diff --git a/pkg/services/object/acl/eacl/v2/eacl_test.go b/pkg/services/object/acl/eacl/v2/eacl_test.go new file mode 100644 index 000000000..bb14cf161 --- /dev/null +++ b/pkg/services/object/acl/eacl/v2/eacl_test.go @@ -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)) +} diff --git a/pkg/services/object/acl/eacl/v2/headers.go b/pkg/services/object/acl/eacl/v2/headers.go new file mode 100644 index 000000000..6cf47f7b0 --- /dev/null +++ b/pkg/services/object/acl/eacl/v2/headers.go @@ -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 +} diff --git a/pkg/services/object/acl/eacl/v2/localstore.go b/pkg/services/object/acl/eacl/v2/localstore.go new file mode 100644 index 000000000..4757ef02c --- /dev/null +++ b/pkg/services/object/acl/eacl/v2/localstore.go @@ -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 +} diff --git a/pkg/services/object/acl/eacl/v2/object.go b/pkg/services/object/acl/eacl/v2/object.go new file mode 100644 index 000000000..e352fb0b6 --- /dev/null +++ b/pkg/services/object/acl/eacl/v2/object.go @@ -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 +} diff --git a/pkg/services/object/acl/eacl/v2/opts.go b/pkg/services/object/acl/eacl/v2/opts.go new file mode 100644 index 000000000..898520fad --- /dev/null +++ b/pkg/services/object/acl/eacl/v2/opts.go @@ -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, + } + } +} diff --git a/pkg/services/object/acl/eacl/v2/xheader.go b/pkg/services/object/acl/eacl/v2/xheader.go new file mode 100644 index 000000000..642c94a76 --- /dev/null +++ b/pkg/services/object/acl/eacl/v2/xheader.go @@ -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 +} diff --git a/pkg/services/object/acl/eacl/validator.go b/pkg/services/object/acl/eacl/validator.go new file mode 100644 index 000000000..a047d8610 --- /dev/null +++ b/pkg/services/object/acl/eacl/validator.go @@ -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() + }, +}