[#1480] objsvc: Remove EACL checks
Signed-off-by: Evgenii Stratonikov <e.stratonikov@yadro.com>
This commit is contained in:
parent
031e12cd18
commit
05c392b050
6 changed files with 1 additions and 309 deletions
|
@ -21,7 +21,6 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/network/cache"
|
||||
objectTransportGRPC "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/network/transport/object/grpc"
|
||||
objectService "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/acl"
|
||||
v2 "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/acl/v2"
|
||||
objectAPE "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/ape"
|
||||
objectwriter "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/common/writer"
|
||||
|
@ -39,7 +38,6 @@ import (
|
|||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/policer"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/replicator"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
eaclSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||
netmapSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
|
@ -458,17 +456,10 @@ func createSplitService(c *cfg, sPutV2 *putsvcV2.Service, sGetV2 *getsvcV2.Servi
|
|||
}
|
||||
|
||||
func createACLServiceV2(c *cfg, apeSvc *objectAPE.Service, irFetcher *cachedIRFetcher) v2.Service {
|
||||
ls := c.cfgObject.cfgLocalStorage.localStorage
|
||||
|
||||
return v2.New(
|
||||
apeSvc,
|
||||
c.netMapSource,
|
||||
irFetcher,
|
||||
acl.NewChecker(
|
||||
c.cfgNetmap.state,
|
||||
c.cfgObject.eaclSource,
|
||||
eaclSDK.NewValidator(),
|
||||
ls),
|
||||
c.cfgObject.cnrSource,
|
||||
v2.WithLogger(c.log),
|
||||
)
|
||||
|
|
|
@ -1,214 +0,0 @@
|
|||
package acl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/netmap"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-node/pkg/local_object_storage/engine"
|
||||
eaclV2 "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/acl/eacl/v2"
|
||||
v2 "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/services/object/acl/v2"
|
||||
bearerSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
|
||||
eaclSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
||||
objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
||||
)
|
||||
|
||||
// Checker implements v2.ACLChecker interfaces and provides
|
||||
// ACL/eACL validation functionality.
|
||||
type Checker struct {
|
||||
eaclSrc container.EACLSource
|
||||
validator *eaclSDK.Validator
|
||||
localStorage *engine.StorageEngine
|
||||
state netmap.State
|
||||
}
|
||||
|
||||
type localStorage struct {
|
||||
ls *engine.StorageEngine
|
||||
}
|
||||
|
||||
func (s *localStorage) Head(ctx context.Context, addr oid.Address) (*objectSDK.Object, error) {
|
||||
if s.ls == nil {
|
||||
return nil, io.ErrUnexpectedEOF
|
||||
}
|
||||
|
||||
return engine.Head(ctx, s.ls, addr)
|
||||
}
|
||||
|
||||
// Various EACL check errors.
|
||||
var (
|
||||
errEACLDeniedByRule = errors.New("denied by rule")
|
||||
errBearerExpired = errors.New("bearer token has expired")
|
||||
errBearerInvalidSignature = errors.New("bearer token has invalid signature")
|
||||
errBearerInvalidContainerID = errors.New("bearer token was created for another container")
|
||||
errBearerNotSignedByOwner = errors.New("bearer token is not signed by the container owner")
|
||||
errBearerInvalidOwner = errors.New("bearer token owner differs from the request sender")
|
||||
)
|
||||
|
||||
// NewChecker creates Checker.
|
||||
// Panics if at least one of the parameter is nil.
|
||||
func NewChecker(
|
||||
state netmap.State,
|
||||
eaclSrc container.EACLSource,
|
||||
validator *eaclSDK.Validator,
|
||||
localStorage *engine.StorageEngine,
|
||||
) *Checker {
|
||||
return &Checker{
|
||||
eaclSrc: eaclSrc,
|
||||
validator: validator,
|
||||
localStorage: localStorage,
|
||||
state: state,
|
||||
}
|
||||
}
|
||||
|
||||
// CheckEACL is a main check function for extended ACL.
|
||||
func (c *Checker) CheckEACL(msg any, reqInfo v2.RequestInfo) error {
|
||||
basicACL := reqInfo.BasicACL()
|
||||
if !basicACL.Extendable() {
|
||||
return nil
|
||||
}
|
||||
|
||||
bearerTok := reqInfo.Bearer()
|
||||
impersonate := bearerTok != nil && bearerTok.Impersonate()
|
||||
|
||||
// if bearer token is not allowed, then ignore it
|
||||
if impersonate || !basicACL.AllowedBearerRules(reqInfo.Operation()) {
|
||||
reqInfo.CleanBearer()
|
||||
}
|
||||
|
||||
var table eaclSDK.Table
|
||||
cnr := reqInfo.ContainerID()
|
||||
|
||||
if bearerTok == nil {
|
||||
eaclInfo, err := c.eaclSrc.GetEACL(cnr)
|
||||
if err != nil {
|
||||
if client.IsErrEACLNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
table = *eaclInfo.Value
|
||||
} else {
|
||||
table = bearerTok.EACLTable()
|
||||
}
|
||||
|
||||
// if bearer token is not present, isValidBearer returns true
|
||||
if err := isValidBearer(reqInfo, c.state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hdrSrc, err := c.getHeaderSource(cnr, msg, reqInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
eaclRole := getRole(reqInfo)
|
||||
|
||||
action, _ := c.validator.CalculateAction(new(eaclSDK.ValidationUnit).
|
||||
WithRole(eaclRole).
|
||||
WithOperation(eaclSDK.Operation(reqInfo.Operation())).
|
||||
WithContainerID(&cnr).
|
||||
WithSenderKey(reqInfo.SenderKey()).
|
||||
WithHeaderSource(hdrSrc).
|
||||
WithEACLTable(&table),
|
||||
)
|
||||
|
||||
if action != eaclSDK.ActionAllow {
|
||||
return errEACLDeniedByRule
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRole(reqInfo v2.RequestInfo) eaclSDK.Role {
|
||||
var eaclRole eaclSDK.Role
|
||||
switch op := reqInfo.RequestRole(); op {
|
||||
default:
|
||||
eaclRole = eaclSDK.Role(op)
|
||||
case acl.RoleOwner:
|
||||
eaclRole = eaclSDK.RoleUser
|
||||
case acl.RoleInnerRing, acl.RoleContainer:
|
||||
eaclRole = eaclSDK.RoleSystem
|
||||
case acl.RoleOthers:
|
||||
eaclRole = eaclSDK.RoleOthers
|
||||
}
|
||||
return eaclRole
|
||||
}
|
||||
|
||||
func (c *Checker) getHeaderSource(cnr cid.ID, msg any, reqInfo v2.RequestInfo) (eaclSDK.TypedHeaderSource, error) {
|
||||
var xHeaderSource eaclV2.XHeaderSource
|
||||
if req, ok := msg.(eaclV2.Request); ok {
|
||||
xHeaderSource = eaclV2.NewRequestXHeaderSource(req)
|
||||
} else {
|
||||
xHeaderSource = eaclV2.NewResponseXHeaderSource(msg.(eaclV2.Response), reqInfo.Request().(eaclV2.Request))
|
||||
}
|
||||
|
||||
hdrSrc, err := eaclV2.NewMessageHeaderSource(&localStorage{ls: c.localStorage}, xHeaderSource, cnr, eaclV2.WithOID(reqInfo.ObjectID()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't parse headers: %w", err)
|
||||
}
|
||||
return hdrSrc, nil
|
||||
}
|
||||
|
||||
// isValidBearer checks whether bearer token was correctly signed by authorized
|
||||
// entity. This method might be defined on whole ACL service because it will
|
||||
// require fetching current epoch to check lifetime.
|
||||
func isValidBearer(reqInfo v2.RequestInfo, st netmap.State) error {
|
||||
ownerCnr := reqInfo.ContainerOwner()
|
||||
|
||||
token := reqInfo.Bearer()
|
||||
|
||||
// 0. Check if bearer token is present in reqInfo.
|
||||
if token == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 1. First check token lifetime. Simplest verification.
|
||||
if token.InvalidAt(st.CurrentEpoch()) {
|
||||
return errBearerExpired
|
||||
}
|
||||
|
||||
// 2. Then check if bearer token is signed correctly.
|
||||
if !token.VerifySignature() {
|
||||
return errBearerInvalidSignature
|
||||
}
|
||||
|
||||
// 3. Then check if container is either empty or equal to the container in the request.
|
||||
cnr, isSet := token.EACLTable().CID()
|
||||
if isSet && !cnr.Equals(reqInfo.ContainerID()) {
|
||||
return errBearerInvalidContainerID
|
||||
}
|
||||
|
||||
// 4. Then check if container owner signed this token.
|
||||
if !bearerSDK.ResolveIssuer(*token).Equals(ownerCnr) {
|
||||
// TODO: #767 in this case we can issue all owner keys from frostfs.id and check once again
|
||||
return errBearerNotSignedByOwner
|
||||
}
|
||||
|
||||
// 5. Then check if request sender has rights to use this token.
|
||||
var keySender frostfsecdsa.PublicKey
|
||||
|
||||
err := keySender.Decode(reqInfo.SenderKey())
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode sender public key: %w", err)
|
||||
}
|
||||
|
||||
var usrSender user.ID
|
||||
user.IDFromKey(&usrSender, ecdsa.PublicKey(keySender))
|
||||
|
||||
if !token.AssertUser(usrSender) {
|
||||
// TODO: #767 in this case we can issue all owner keys from frostfs.id and check once again
|
||||
return errBearerInvalidOwner
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -2,8 +2,6 @@ package v2
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
)
|
||||
|
||||
const invalidRequestMessage = "malformed request"
|
||||
|
@ -20,15 +18,3 @@ var (
|
|||
errInvalidSessionOwner = malformedRequestError("invalid session token owner")
|
||||
errInvalidVerb = malformedRequestError("session token verb is invalid")
|
||||
)
|
||||
|
||||
const (
|
||||
accessDeniedACLReasonFmt = "access to operation %s is denied by basic ACL check"
|
||||
accessDeniedEACLReasonFmt = "access to operation %s is denied by extended ACL check: %v"
|
||||
)
|
||||
|
||||
func eACLErr(info RequestInfo, err error) error {
|
||||
errAccessDenied := &apistatus.ObjectAccessDenied{}
|
||||
errAccessDenied.WriteReason(fmt.Sprintf(accessDeniedEACLReasonFmt, info.operation, err))
|
||||
|
||||
return errAccessDenied
|
||||
}
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
package v2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEACLErr(t *testing.T) {
|
||||
var reqInfo RequestInfo
|
||||
testErr := errors.New("test-eacl")
|
||||
err := eACLErr(reqInfo, testErr)
|
||||
|
||||
var errAccessDenied *apistatus.ObjectAccessDenied
|
||||
|
||||
require.ErrorAs(t, err, &errAccessDenied,
|
||||
"eACLErr must be able to be casted to apistatus.ObjectAccessDenied")
|
||||
}
|
|
@ -42,27 +42,15 @@ type patchStreamBasicChecker struct {
|
|||
}
|
||||
|
||||
type getStreamBasicChecker struct {
|
||||
checker ACLChecker
|
||||
|
||||
object.GetObjectStream
|
||||
|
||||
info RequestInfo
|
||||
}
|
||||
|
||||
type rangeStreamBasicChecker struct {
|
||||
checker ACLChecker
|
||||
|
||||
object.GetObjectRangeStream
|
||||
|
||||
info RequestInfo
|
||||
}
|
||||
|
||||
type searchStreamBasicChecker struct {
|
||||
checker ACLChecker
|
||||
|
||||
object.SearchStream
|
||||
|
||||
info RequestInfo
|
||||
}
|
||||
|
||||
// Option represents Service constructor option.
|
||||
|
@ -73,8 +61,6 @@ type cfg struct {
|
|||
|
||||
containers container.Source
|
||||
|
||||
checker ACLChecker
|
||||
|
||||
irFetcher InnerRingFetcher
|
||||
|
||||
nm netmap.Source
|
||||
|
@ -86,7 +72,6 @@ type cfg struct {
|
|||
func New(next object.ServiceServer,
|
||||
nm netmap.Source,
|
||||
irf InnerRingFetcher,
|
||||
acl ACLChecker,
|
||||
cs container.Source,
|
||||
opts ...Option,
|
||||
) Service {
|
||||
|
@ -95,7 +80,6 @@ func New(next object.ServiceServer,
|
|||
next: next,
|
||||
nm: nm,
|
||||
irFetcher: irf,
|
||||
checker: acl,
|
||||
containers: cs,
|
||||
}
|
||||
|
||||
|
@ -230,8 +214,6 @@ func (b Service) Get(request *objectV2.GetRequest, stream object.GetObjectStream
|
|||
|
||||
return b.next.Get(request, &getStreamBasicChecker{
|
||||
GetObjectStream: newWrappedGetObjectStreamStream(stream, reqInfo),
|
||||
info: reqInfo,
|
||||
checker: b.checker,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -298,14 +280,7 @@ func (b Service) Head(
|
|||
|
||||
reqInfo.obj = obj
|
||||
|
||||
resp, err := b.next.Head(requestContext(ctx, reqInfo), request)
|
||||
if err == nil {
|
||||
if err = b.checker.CheckEACL(resp, reqInfo); err != nil {
|
||||
err = eACLErr(reqInfo, err)
|
||||
}
|
||||
}
|
||||
|
||||
return resp, err
|
||||
return b.next.Head(requestContext(ctx, reqInfo), request)
|
||||
}
|
||||
|
||||
func (b Service) Search(request *objectV2.SearchRequest, stream object.SearchStream) error {
|
||||
|
@ -344,9 +319,7 @@ func (b Service) Search(request *objectV2.SearchRequest, stream object.SearchStr
|
|||
}
|
||||
|
||||
return b.next.Search(request, &searchStreamBasicChecker{
|
||||
checker: b.checker,
|
||||
SearchStream: newWrappedSearchStream(stream, reqInfo),
|
||||
info: reqInfo,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -441,9 +414,7 @@ func (b Service) GetRange(request *objectV2.GetRangeRequest, stream object.GetOb
|
|||
reqInfo.obj = obj
|
||||
|
||||
return b.next.GetRange(request, &rangeStreamBasicChecker{
|
||||
checker: b.checker,
|
||||
GetObjectRangeStream: newWrappedRangeStream(stream, reqInfo),
|
||||
info: reqInfo,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -657,28 +628,14 @@ func (p putStreamBasicChecker) CloseAndRecv(ctx context.Context) (*objectV2.PutR
|
|||
}
|
||||
|
||||
func (g *getStreamBasicChecker) Send(resp *objectV2.GetResponse) error {
|
||||
if _, ok := resp.GetBody().GetObjectPart().(*objectV2.GetObjectPartInit); ok {
|
||||
if err := g.checker.CheckEACL(resp, g.info); err != nil {
|
||||
return eACLErr(g.info, err)
|
||||
}
|
||||
}
|
||||
|
||||
return g.GetObjectStream.Send(resp)
|
||||
}
|
||||
|
||||
func (g *rangeStreamBasicChecker) Send(resp *objectV2.GetRangeResponse) error {
|
||||
if err := g.checker.CheckEACL(resp, g.info); err != nil {
|
||||
return eACLErr(g.info, err)
|
||||
}
|
||||
|
||||
return g.GetObjectRangeStream.Send(resp)
|
||||
}
|
||||
|
||||
func (g *searchStreamBasicChecker) Send(resp *objectV2.SearchResponse) error {
|
||||
if err := g.checker.CheckEACL(resp, g.info); err != nil {
|
||||
return eACLErr(g.info, err)
|
||||
}
|
||||
|
||||
return g.SearchStream.Send(resp)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,5 @@
|
|||
package v2
|
||||
|
||||
// ACLChecker is an interface that must provide
|
||||
// ACL related checks.
|
||||
type ACLChecker interface {
|
||||
// CheckEACL must return non-nil error if request
|
||||
// doesn't pass extended ACL validation.
|
||||
CheckEACL(any, RequestInfo) error
|
||||
}
|
||||
|
||||
// InnerRingFetcher is an interface that must provide
|
||||
// Inner Ring information.
|
||||
type InnerRingFetcher interface {
|
||||
|
|
Loading…
Reference in a new issue