[#36] eacl: add eACL checking algorithm from neofs-node
Signed-off-by: Evgenii Stratonikov <evgeniy@nspcc.ru>
This commit is contained in:
parent
ea5da31bd2
commit
3a0c9e4542
4 changed files with 372 additions and 0 deletions
34
eacl/opts.go
Normal file
34
eacl/opts.go
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
111
eacl/types.go
Normal file
111
eacl/types.go
Normal file
|
@ -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
|
||||||
|
}
|
171
eacl/validator.go
Normal file
171
eacl/validator.go
Normal file
|
@ -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()
|
||||||
|
},
|
||||||
|
}
|
56
eacl/validator_test.go
Normal file
56
eacl/validator_test.go
Normal file
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue