From 1dd4d48b5f8057cfc69df599403d4087fe61cf35 Mon Sep 17 00:00:00 2001 From: Pavel Korotkov Date: Tue, 7 Jul 2020 18:13:56 +0300 Subject: [PATCH] alc: add a rich functionality to manage Extended ACL --- acl/action.go | 38 ++++ acl/action_test.go | 76 +++++++ acl/extended.go | 120 +++++++++++ acl/header.go | 290 +++++++++++++++++++++++++ acl/headers_test.go | 59 +++++ acl/match.go | 29 +++ acl/match_test.go | 44 ++++ acl/types.go | 56 +++++ acl/types.pb.go | Bin 3241 -> 37624 bytes acl/types.proto | 79 +++++++ acl/wrappers.go | 498 +++++++++++++++++++++++++++++++++++++++++++ acl/wrappers_test.go | 139 ++++++++++++ 12 files changed, 1428 insertions(+) create mode 100644 acl/action.go create mode 100644 acl/action_test.go create mode 100644 acl/extended.go create mode 100644 acl/header.go create mode 100644 acl/headers_test.go create mode 100644 acl/match.go create mode 100644 acl/match_test.go create mode 100644 acl/types.go create mode 100644 acl/wrappers.go create mode 100644 acl/wrappers_test.go diff --git a/acl/action.go b/acl/action.go new file mode 100644 index 0000000..b2986e2 --- /dev/null +++ b/acl/action.go @@ -0,0 +1,38 @@ +package acl + +// RequestInfo is an interface of request information needed for extended ACL check. +type RequestInfo interface { + TypedHeaderSource + + // Must return the binary representation of request initiator's key. + Key() []byte + + // Must return true if request corresponds to operation type. + TypeOf(OperationType) bool + + // Must return true if request has passed target. + TargetOf(Target) bool +} + +// ExtendedACLChecker is an interface of extended ACL checking tool. +type ExtendedACLChecker interface { + // Must return an action according to the results of applying the ACL table rules to request. + // + // Must return ActionUndefined if it is unable to explicitly calculate the action. + Action(ExtendedACLTable, RequestInfo) ExtendedACLAction +} + +type extendedACLChecker struct{} + +const ( + // ActionUndefined is ExtendedACLAction used to mark value as undefined. + // Most of the tools consider ActionUndefined as incalculable. + // Using ActionUndefined in ExtendedACLRecord is unsafe. + ActionUndefined ExtendedACLAction = iota + + // ActionAllow is ExtendedACLAction used to mark an applicability of ACL rule. + ActionAllow + + // ActionDeny is ExtendedACLAction used to mark an inapplicability of ACL rule. + ActionDeny +) diff --git a/acl/action_test.go b/acl/action_test.go new file mode 100644 index 0000000..83022e3 --- /dev/null +++ b/acl/action_test.go @@ -0,0 +1,76 @@ +package acl + +type testExtendedACLTable struct { + records []ExtendedACLRecord +} + +type testRequestInfo struct { + headers []TypedHeader + key []byte + opType OperationType + target Target +} + +type testEACLRecord struct { + opType OperationType + filters []HeaderFilter + targets []ExtendedACLTarget + action ExtendedACLAction +} + +type testEACLTarget struct { + target Target + keys [][]byte +} + +func (s testEACLTarget) Target() Target { + return s.target +} + +func (s testEACLTarget) KeyList() [][]byte { + return s.keys +} + +func (s testEACLRecord) OperationType() OperationType { + return s.opType +} + +func (s testEACLRecord) HeaderFilters() []HeaderFilter { + return s.filters +} + +func (s testEACLRecord) TargetList() []ExtendedACLTarget { + return s.targets +} + +func (s testEACLRecord) Action() ExtendedACLAction { + return s.action +} + +func (s testRequestInfo) HeadersOfType(typ HeaderType) ([]Header, bool) { + res := make([]Header, 0, len(s.headers)) + + for i := range s.headers { + if s.headers[i].HeaderType() == typ { + res = append(res, s.headers[i]) + } + } + + return res, true +} + +func (s testRequestInfo) Key() []byte { + return s.key +} + +func (s testRequestInfo) TypeOf(t OperationType) bool { + return s.opType == t +} + +func (s testRequestInfo) TargetOf(t Target) bool { + return s.target == t +} + +func (s testExtendedACLTable) Records() []ExtendedACLRecord { + return s.records +} diff --git a/acl/extended.go b/acl/extended.go new file mode 100644 index 0000000..61ccbcf --- /dev/null +++ b/acl/extended.go @@ -0,0 +1,120 @@ +package acl + +import ( + "context" + + "github.com/nspcc-dev/neofs-api-go/refs" +) + +// OperationType is an enumeration of operation types for extended ACL. +type OperationType uint32 + +// HeaderType is an enumeration of header types for extended ACL. +type HeaderType uint32 + +// MatchType is an enumeration of match types for extended ACL. +type MatchType uint32 + +// ExtendedACLAction is an enumeration of extended ACL actions. +type ExtendedACLAction uint32 + +// Header is an interface of string key-value pair, +type Header interface { + // Must return string identifier of header. + Name() string + + // Must return string value of header. + Value() string +} + +// TypedHeader is an interface of Header and HeaderType pair. +type TypedHeader interface { + Header + + // Must return type of filtered header. + HeaderType() HeaderType +} + +// TypedHeaderSource is a various types of header set interface. +type TypedHeaderSource interface { + // Must return list of Header of particular type. + // Must return false if there is no ability to compose header list. + HeadersOfType(HeaderType) ([]Header, bool) +} + +// HeaderFilter is an interface of grouped information about filtered header. +type HeaderFilter interface { + // Must return match type of filter. + MatchType() MatchType + + TypedHeader +} + +// ExtendedACLTarget is an interface of grouped information about extended ACL rule target. +type ExtendedACLTarget interface { + // Must return ACL target type. + Target() Target + + // Must return public key list of ACL targets. + KeyList() [][]byte +} + +// ExtendedACLRecord is an interface of record of extended ACL rule table. +type ExtendedACLRecord interface { + // Must return operation type of extended ACL rule. + OperationType() OperationType + + // Must return list of header filters of extended ACL rule. + HeaderFilters() []HeaderFilter + + // Must return target list of extended ACL rule. + TargetList() []ExtendedACLTarget + + // Must return action of extended ACL rule. + Action() ExtendedACLAction +} + +// ExtendedACLTable is an interface of extended ACL table. +type ExtendedACLTable interface { + // Must return list of extended ACL rules. + Records() []ExtendedACLRecord +} + +// ExtendedACLSource is an interface of storage of extended ACL tables with read access. +type ExtendedACLSource interface { + // Must return extended ACL table by container ID key. + GetExtendedACLTable(context.Context, refs.CID) (ExtendedACLTable, error) +} + +// ExtendedACLStore is an interface of storage of extended ACL tables. +type ExtendedACLStore interface { + ExtendedACLSource + + // Must store extended ACL table for container ID key. + PutExtendedACLTable(context.Context, refs.CID, ExtendedACLTable) error +} + +const ( + _ OperationType = iota + + // OpTypeGet is an OperationType for object.Get RPC + OpTypeGet + + // OpTypePut is an OperationType for object.Put RPC + OpTypePut + + // OpTypeHead is an OperationType for object.Head RPC + OpTypeHead + + // OpTypeSearch is an OperationType for object.Search RPC + OpTypeSearch + + // OpTypeDelete is an OperationType for object.Delete RPC + OpTypeDelete + + // OpTypeRange is an OperationType for object.GetRange RPC + OpTypeRange + + // OpTypeRangeHash is an OperationType for object.GetRangeHash RPC + OpTypeRangeHash +) diff --git a/acl/header.go b/acl/header.go new file mode 100644 index 0000000..9dff79e --- /dev/null +++ b/acl/header.go @@ -0,0 +1,290 @@ +package acl + +import ( + "strconv" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/nspcc-dev/neofs-api-go/service" +) + +type objectHeaderSource struct { + obj *object.Object +} + +type typedHeader struct { + n string + v string + t HeaderType +} + +type extendedHeadersWrapper struct { + hdrSrc service.ExtendedHeadersSource +} + +type typedExtendedHeader struct { + hdr service.ExtendedHeader +} + +const ( + _ HeaderType = iota + + // HdrTypeRequest is a HeaderType for request header. + HdrTypeRequest + + // HdrTypeObjSys is a HeaderType for system headers of object. + HdrTypeObjSys + + // HdrTypeObjUsr is a HeaderType for user headers of object. + HdrTypeObjUsr +) + +const ( + // HdrObjSysNameID is a name of ID field in system header of object. + HdrObjSysNameID = "ID" + + // HdrObjSysNameCID is a name of CID field in system header of object. + HdrObjSysNameCID = "CID" + + // HdrObjSysNameOwnerID is a name of OwnerID field in system header of object. + HdrObjSysNameOwnerID = "OWNER_ID" + + // HdrObjSysNameVersion is a name of Version field in system header of object. + HdrObjSysNameVersion = "VERSION" + + // HdrObjSysNamePayloadLength is a name of PayloadLength field in system header of object. + HdrObjSysNamePayloadLength = "PAYLOAD_LENGTH" + + // HdrObjSysNameCreatedUnix is a name of CreatedAt.UnitTime field in system header of object. + HdrObjSysNameCreatedUnix = "CREATED_UNIX" + + // HdrObjSysNameCreatedEpoch is a name of CreatedAt.Epoch field in system header of object. + HdrObjSysNameCreatedEpoch = "CREATED_EPOCH" + + // HdrObjSysLinkPrev is a name of previous link header in extended headers of object. + HdrObjSysLinkPrev = "LINK_PREV" + + // HdrObjSysLinkNext is a name of next link header in extended headers of object. + HdrObjSysLinkNext = "LINK_NEXT" + + // HdrObjSysLinkChild is a name of child link header in extended headers of object. + HdrObjSysLinkChild = "LINK_CHILD" + + // HdrObjSysLinkPar is a name of parent link header in extended headers of object. + HdrObjSysLinkPar = "LINK_PAR" + + // HdrObjSysLinkSG is a name of storage group link header in extended headers of object. + HdrObjSysLinkSG = "LINK_SG" +) + +func newTypedHeader(name, value string, typ HeaderType) TypedHeader { + return &typedHeader{ + n: name, + v: value, + t: typ, + } +} + +// Name is a name field getter. +func (s typedHeader) Name() string { + return s.n +} + +// Value is a value field getter. +func (s typedHeader) Value() string { + return s.v +} + +// HeaderType is a type field getter. +func (s typedHeader) HeaderType() HeaderType { + return s.t +} + +// TypedHeaderSourceFromObject wraps passed object and returns TypedHeaderSource interface. +func TypedHeaderSourceFromObject(obj *object.Object) TypedHeaderSource { + return &objectHeaderSource{ + obj: obj, + } +} + +// HeaderOfType gathers object headers of passed type and returns Header list. +// +// If value of some header can not be calculated (e.g. nil extended header), it does not appear in list. +// +// Always returns true. +func (s objectHeaderSource) HeadersOfType(typ HeaderType) ([]Header, bool) { + if s.obj == nil { + return nil, true + } + + var res []Header + + switch typ { + case HdrTypeObjUsr: + objHeaders := s.obj.GetHeaders() + + res = make([]Header, 0, len(objHeaders)) // 7 system header fields + + for _, extHdr := range objHeaders { + if h := newTypedObjectExtendedHeader(extHdr); h != nil { + res = append(res, h) + } + } + case HdrTypeObjSys: + res = make([]Header, 0, 7) + + sysHdr := s.obj.GetSystemHeader() + + // ID + res = append(res, newTypedHeader( + HdrObjSysNameID, + sysHdr.ID.String(), + HdrTypeObjSys), + ) + + // CID + res = append(res, newTypedHeader( + HdrObjSysNameCID, + sysHdr.CID.String(), + HdrTypeObjSys), + ) + + // OwnerID + res = append(res, newTypedHeader( + HdrObjSysNameOwnerID, + sysHdr.OwnerID.String(), + HdrTypeObjSys), + ) + + // Version + res = append(res, newTypedHeader( + HdrObjSysNameVersion, + strconv.FormatUint(sysHdr.GetVersion(), 10), + HdrTypeObjSys), + ) + + // PayloadLength + res = append(res, newTypedHeader( + HdrObjSysNamePayloadLength, + strconv.FormatUint(sysHdr.GetPayloadLength(), 10), + HdrTypeObjSys), + ) + + created := sysHdr.GetCreatedAt() + + // CreatedAt.UnitTime + res = append(res, newTypedHeader( + HdrObjSysNameCreatedUnix, + strconv.FormatUint(uint64(created.GetUnixTime()), 10), + HdrTypeObjSys), + ) + + // CreatedAt.Epoch + res = append(res, newTypedHeader( + HdrObjSysNameCreatedEpoch, + strconv.FormatUint(created.GetEpoch(), 10), + HdrTypeObjSys), + ) + } + + return res, true +} + +func newTypedObjectExtendedHeader(h object.Header) TypedHeader { + val := h.GetValue() + if val == nil { + return nil + } + + res := new(typedHeader) + res.t = HdrTypeObjSys + + switch hdr := val.(type) { + case *object.Header_UserHeader: + if hdr.UserHeader == nil { + return nil + } + + res.t = HdrTypeObjUsr + res.n = hdr.UserHeader.GetKey() + res.v = hdr.UserHeader.GetValue() + case *object.Header_Link: + if hdr.Link == nil { + return nil + } + + switch hdr.Link.GetType() { + case object.Link_Previous: + res.n = HdrObjSysLinkPrev + case object.Link_Next: + res.n = HdrObjSysLinkNext + case object.Link_Child: + res.n = HdrObjSysLinkChild + case object.Link_Parent: + res.n = HdrObjSysLinkPar + case object.Link_StorageGroup: + res.n = HdrObjSysLinkSG + default: + return nil + } + + res.v = hdr.Link.ID.String() + default: + return nil + } + + return res +} + +// TypedHeaderSourceFromExtendedHeaders wraps passed ExtendedHeadersSource and returns TypedHeaderSource interface. +func TypedHeaderSourceFromExtendedHeaders(hdrSrc service.ExtendedHeadersSource) TypedHeaderSource { + return &extendedHeadersWrapper{ + hdrSrc: hdrSrc, + } +} + +// Name returns the result of Key method. +func (s typedExtendedHeader) Name() string { + return s.hdr.Key() +} + +// Value returns the result of Value method. +func (s typedExtendedHeader) Value() string { + return s.hdr.Value() +} + +// HeaderType always returns HdrTypeRequest. +func (s typedExtendedHeader) HeaderType() HeaderType { + return HdrTypeRequest +} + +// TypedHeaders gathers extended request headers and returns TypedHeader list. +// +// Nil headers are ignored. +// +// Always returns true. +func (s extendedHeadersWrapper) HeadersOfType(typ HeaderType) ([]Header, bool) { + if s.hdrSrc == nil { + return nil, true + } + + var res []Header + + switch typ { + case HdrTypeRequest: + hs := s.hdrSrc.ExtendedHeaders() + + res = make([]Header, 0, len(hs)) + + for i := range hs { + if hs[i] == nil { + continue + } + + res = append(res, &typedExtendedHeader{ + hdr: hs[i], + }) + } + } + + return res, true +} diff --git a/acl/headers_test.go b/acl/headers_test.go new file mode 100644 index 0000000..500d9a4 --- /dev/null +++ b/acl/headers_test.go @@ -0,0 +1,59 @@ +package acl + +import ( + "testing" + + "github.com/nspcc-dev/neofs-api-go/object" + "github.com/stretchr/testify/require" +) + +func TestNewTypedObjectExtendedHeader(t *testing.T) { + var res TypedHeader + + hdr := object.Header{} + + // nil value + require.Nil(t, newTypedObjectExtendedHeader(hdr)) + + // UserHeader + { + key := "key" + val := "val" + hdr.Value = &object.Header_UserHeader{ + UserHeader: &object.UserHeader{ + Key: key, + Value: val, + }, + } + + res = newTypedObjectExtendedHeader(hdr) + require.Equal(t, HdrTypeObjUsr, res.HeaderType()) + require.Equal(t, key, res.Name()) + require.Equal(t, val, res.Value()) + } + + { // Link + link := new(object.Link) + link.ID = object.ID{1, 2, 3} + + hdr.Value = &object.Header_Link{ + Link: link, + } + + check := func(lt object.Link_Type, name string) { + link.Type = lt + + res = newTypedObjectExtendedHeader(hdr) + + require.Equal(t, HdrTypeObjSys, res.HeaderType()) + require.Equal(t, name, res.Name()) + require.Equal(t, link.ID.String(), res.Value()) + } + + check(object.Link_Previous, HdrObjSysLinkPrev) + check(object.Link_Next, HdrObjSysLinkNext) + check(object.Link_Parent, HdrObjSysLinkPar) + check(object.Link_Child, HdrObjSysLinkChild) + check(object.Link_StorageGroup, HdrObjSysLinkSG) + } +} diff --git a/acl/match.go b/acl/match.go new file mode 100644 index 0000000..bddee89 --- /dev/null +++ b/acl/match.go @@ -0,0 +1,29 @@ +package acl + +const ( + _ MatchType = iota + StringEqual + StringNotEqual +) + +// Maps MatchType to corresponding function. +// 1st argument of function - header value, 2nd - header filter. +var mMatchFns = map[MatchType]func(Header, Header) bool{ + StringEqual: stringEqual, + + StringNotEqual: stringNotEqual, +} + +const ( + mResUndefined = iota + mResMatch + mResMismatch +) + +func stringEqual(header, filter Header) bool { + return header.Value() == filter.Value() +} + +func stringNotEqual(header, filter Header) bool { + return header.Value() != filter.Value() +} diff --git a/acl/match_test.go b/acl/match_test.go new file mode 100644 index 0000000..6975722 --- /dev/null +++ b/acl/match_test.go @@ -0,0 +1,44 @@ +package acl + +type testTypedHeader struct { + t HeaderType + k string + v string +} + +type testHeaderSrc struct { + hs []TypedHeader +} + +type testHeaderFilter struct { + TypedHeader + t MatchType +} + +func (s testHeaderFilter) MatchType() MatchType { + return s.t +} + +func (s testHeaderSrc) HeadersOfType(typ HeaderType) ([]Header, bool) { + res := make([]Header, 0, len(s.hs)) + + for i := range s.hs { + if s.hs[i].HeaderType() == typ { + res = append(res, s.hs[i]) + } + } + + return res, true +} + +func (s testTypedHeader) Name() string { + return s.k +} + +func (s testTypedHeader) Value() string { + return s.v +} + +func (s testTypedHeader) HeaderType() HeaderType { + return s.t +} diff --git a/acl/types.go b/acl/types.go new file mode 100644 index 0000000..0587b9b --- /dev/null +++ b/acl/types.go @@ -0,0 +1,56 @@ +package acl + +// SetMatchType is MatchType field setter. +func (m *EACLRecord_FilterInfo) SetMatchType(v EACLRecord_FilterInfo_MatchType) { + m.MatchType = v +} + +// SetHeader is a Header field setter. +func (m *EACLRecord_FilterInfo) SetHeader(v EACLRecord_FilterInfo_Header) { + m.Header = v +} + +// SetHeaderName is a HeaderName field setter. +func (m *EACLRecord_FilterInfo) SetHeaderName(v string) { + m.HeaderName = v +} + +// SetHeaderVal is a HeaderVal field setter. +func (m *EACLRecord_FilterInfo) SetHeaderVal(v string) { + m.HeaderVal = v +} + +// SetTarget is a Target field setter. +func (m *EACLRecord_TargetInfo) SetTarget(v Target) { + m.Target = v +} + +// SetKeyList is a KeyList field setter. +func (m *EACLRecord_TargetInfo) SetKeyList(v [][]byte) { + m.KeyList = v +} + +// SetOperation is an Operation field setter. +func (m *EACLRecord) SetOperation(v EACLRecord_Operation) { + m.Operation = v +} + +// SetAction is an Action field setter. +func (m *EACLRecord) SetAction(v EACLRecord_Action) { + m.Action = v +} + +// SetFilters is a Filters field setter. +func (m *EACLRecord) SetFilters(v []*EACLRecord_FilterInfo) { + m.Filters = v +} + +// SetTargets is a Targets field setter. +func (m *EACLRecord) SetTargets(v []*EACLRecord_TargetInfo) { + m.Targets = v +} + +// SetRecords is a Records field setter. +func (m *EACLTable) SetRecords(v []*EACLRecord) { + m.Records = v +} diff --git a/acl/types.pb.go b/acl/types.pb.go index bcbd1998e272c6b48d814cc10e11139b235079c4..24ecf2814a556e10d7cb3b1a23647c4150bebd37 100644 GIT binary patch literal 37624 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