diff --git a/eacl/opts.go b/eacl/opts.go new file mode 100644 index 0000000..530fa76 --- /dev/null +++ b/eacl/opts.go @@ -0,0 +1,34 @@ +package eacl + +import ( + "go.uber.org/zap" +) + +// Option represents Validator option. +type Option func(*cfg) + +type cfg struct { + logger *zap.Logger + + storage Source +} + +func defaultCfg() *cfg { + return &cfg{ + logger: zap.L(), + } +} + +// WithLogger configures the Validator to use logger v. +func WithLogger(v *zap.Logger) Option { + return func(c *cfg) { + c.logger = v + } +} + +// WithEACLSource configures the validator to use v as eACL source. +func WithEACLSource(v Source) Option { + return func(c *cfg) { + c.storage = v + } +} diff --git a/eacl/types.go b/eacl/types.go new file mode 100644 index 0000000..10abdd5 --- /dev/null +++ b/eacl/types.go @@ -0,0 +1,111 @@ +package eacl + +import ( + "errors" + + bearer "github.com/nspcc-dev/neofs-api-go/v2/acl" + cid "github.com/nspcc-dev/neofs-sdk-go/container/id" +) + +// Source is the interface that wraps +// basic methods of extended ACL table source. +type Source interface { + // GetEACL reads the table from the source by identifier. + // It returns any error encountered. + // + // GetEACL must return exactly one non-nil value. + // + // Must return pkg/core/container.ErrEACLNotFound if requested + // eACL table is not in source. + GetEACL(*cid.ID) (*Table, error) +} + +// Header is an interface of string key-value header. +type Header interface { + Key() string + Value() 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(FilterHeaderType) ([]Header, bool) +} + +// ValidationUnit represents unit of check for Validator. +type ValidationUnit struct { + cid *cid.ID + + role Role + + op Operation + + hdrSrc TypedHeaderSource + + key []byte + + bearer *bearer.BearerToken +} + +// ErrEACLNotFound is returned by eACL storage implementations when +// requested eACL table is not in storage. +var ErrEACLNotFound = errors.New("extended ACL table is not set for this container") + +// WithContainerID configures ValidationUnit to use v as request's container ID. +func (u *ValidationUnit) WithContainerID(v *cid.ID) *ValidationUnit { + if u != nil { + u.cid = v + } + + return u +} + +// WithRole configures ValidationUnit to use v as request's role. +func (u *ValidationUnit) WithRole(v Role) *ValidationUnit { + if u != nil { + u.role = v + } + + return u +} + +// WithOperation configures ValidationUnit to use v as request's operation. +func (u *ValidationUnit) WithOperation(v Operation) *ValidationUnit { + if u != nil { + u.op = v + } + + return u +} + +// WithHeaderSource configures ValidationUnit to use v as a source of headers. +func (u *ValidationUnit) WithHeaderSource(v TypedHeaderSource) *ValidationUnit { + if u != nil { + u.hdrSrc = v + } + + return u +} + +// WithSenderKey configures ValidationUnit to use as sender's public key. +func (u *ValidationUnit) WithSenderKey(v []byte) *ValidationUnit { + if u != nil { + u.key = v + } + + return u +} + +// WithBearerToken configures ValidationUnit to use v as request's bearer token. +func (u *ValidationUnit) WithBearerToken(bearer *bearer.BearerToken) *ValidationUnit { + if u != nil { + u.bearer = bearer + } + + return u +} diff --git a/eacl/validator.go b/eacl/validator.go new file mode 100644 index 0000000..c14c6d7 --- /dev/null +++ b/eacl/validator.go @@ -0,0 +1,171 @@ +package eacl + +import ( + "bytes" + "errors" + + "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 +} + +// 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, +// ActionUnknown is returned. +// +// If no matching table entry is found, ActionAllow is returned. +func (v *Validator) CalculateAction(unit *ValidationUnit) Action { + var ( + err error + table *Table + ) + + if unit.bearer != nil { + table = NewTableFromV2(unit.bearer.GetBody().GetEACL()) + } else { + // get eACL table by container ID + table, err = v.storage.GetEACL(unit.cid) + if err != nil { + if errors.Is(err, ErrEACLNotFound) { + return ActionAllow + } + + v.logger.Error("could not get eACL table", + zap.String("error", err.Error()), + ) + + return ActionUnknown + } + } + + return tableAction(unit, table) +} + +// tableAction calculates action on the request based on the eACL rules. +func tableAction(unit *ValidationUnit, table *Table) 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 ActionAllow + case val == 0: + return record.Action() + } + } + + return 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 []*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.Key() != filter.Key() { + 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 *Record) bool { + for _, target := range record.Targets() { + // check public key match + if pubs := target.BinaryKeys(); len(pubs) != 0 { + for _, key := range pubs { + if bytes.Equal(key, unit.key) { + return true + } + } + continue + } + + // check target group match + if unit.role == target.Role() { + return true + } + } + + return false +} + +// Maps match type to corresponding function. +var mMatchFns = map[Match]func(Header, *Filter) bool{ + MatchStringEqual: func(header Header, filter *Filter) bool { + return header.Value() == filter.Value() + }, + + MatchStringNotEqual: func(header Header, filter *Filter) bool { + return header.Value() != filter.Value() + }, +} diff --git a/eacl/validator_test.go b/eacl/validator_test.go new file mode 100644 index 0000000..4cae364 --- /dev/null +++ b/eacl/validator_test.go @@ -0,0 +1,56 @@ +package eacl + +import ( + "math/rand" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestTargetMatches(t *testing.T) { + pubs := makeKeys(t, 3) + + tgt1 := NewTarget() + tgt1.SetBinaryKeys(pubs[0:2]) + tgt1.SetRole(RoleUser) + + tgt2 := NewTarget() + tgt2.SetRole(RoleOthers) + + r := NewRecord() + r.SetTargets(tgt1, tgt2) + + u := newValidationUnit(RoleUser, pubs[0]) + require.True(t, targetMatches(u, r)) + + u = newValidationUnit(RoleUser, pubs[2]) + require.False(t, targetMatches(u, r)) + + u = newValidationUnit(RoleUnknown, pubs[1]) + require.True(t, targetMatches(u, r)) + + u = newValidationUnit(RoleOthers, pubs[2]) + require.True(t, targetMatches(u, r)) + + u = newValidationUnit(RoleSystem, pubs[2]) + require.False(t, targetMatches(u, r)) +} + +func makeKeys(t *testing.T, n int) [][]byte { + pubs := make([][]byte, n) + for i := range pubs { + pubs[i] = make([]byte, 33) + pubs[i][0] = 0x02 + + _, err := rand.Read(pubs[i][1:]) + require.NoError(t, err) + } + return pubs +} + +func newValidationUnit(role Role, key []byte) *ValidationUnit { + return &ValidationUnit{ + role: role, + key: key, + } +}