Dmitrii Stepanov
18d8898b00
Pass required deps as args. Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
262 lines
7.4 KiB
Go
262 lines
7.4 KiB
Go
package acl
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"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"
|
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
)
|
|
|
|
// 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,
|
|
}
|
|
}
|
|
|
|
// CheckBasicACL is a main check function for basic ACL.
|
|
func (c *Checker) CheckBasicACL(info v2.RequestInfo) bool {
|
|
// check basic ACL permissions
|
|
return info.BasicACL().IsOpAllowed(info.Operation(), info.RequestRole())
|
|
}
|
|
|
|
// StickyBitCheck validates owner field in the request if sticky bit is enabled.
|
|
func (c *Checker) StickyBitCheck(info v2.RequestInfo, owner user.ID) bool {
|
|
// According to FrostFS specification sticky bit has no effect on system nodes
|
|
// for correct intra-container work with objects (in particular, replication).
|
|
if info.RequestRole() == acl.RoleContainer {
|
|
return true
|
|
}
|
|
|
|
if !info.BasicACL().Sticky() {
|
|
return true
|
|
}
|
|
|
|
if len(info.SenderKey()) == 0 {
|
|
return false
|
|
}
|
|
|
|
requestSenderKey := unmarshalPublicKey(info.SenderKey())
|
|
|
|
return isOwnerFromKey(owner, requestSenderKey)
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func isOwnerFromKey(id user.ID, key *keys.PublicKey) bool {
|
|
if key == nil {
|
|
return false
|
|
}
|
|
|
|
var id2 user.ID
|
|
user.IDFromKey(&id2, (ecdsa.PublicKey)(*key))
|
|
|
|
return id.Equals(id2)
|
|
}
|
|
|
|
func unmarshalPublicKey(bs []byte) *keys.PublicKey {
|
|
pub, err := keys.NewPublicKeyFromBytes(bs, elliptic.P256())
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return pub
|
|
}
|