forked from TrueCloudLab/frostfs-node
aarifullin
04aa7874b2
* Make `verifyClient` method perform APE check if a container was created with zero-filled basic ACL. * Object verbs are used in APE, until tree verbs are introduced. Signed-off-by: Airat Arifullin <a.arifullin@yadro.com>
331 lines
8.3 KiB
Go
331 lines
8.3 KiB
Go
package tree
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/refs"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-node/internal/logs"
|
|
core "git.frostfs.info/TrueCloudLab/frostfs-node/pkg/core/container"
|
|
"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"
|
|
cidSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
frostfscrypto "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto"
|
|
frostfsecdsa "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/crypto/ecdsa"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/eacl"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
|
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type message interface {
|
|
SignedDataSize() int
|
|
ReadSignedData([]byte) ([]byte, error)
|
|
GetSignature() *Signature
|
|
SetSignature(*Signature)
|
|
}
|
|
|
|
func basicACLErr(op acl.Op) error {
|
|
return fmt.Errorf("access to operation %s is denied by basic ACL check", op)
|
|
}
|
|
|
|
func eACLErr(op eacl.Operation, err error) error {
|
|
return fmt.Errorf("access to operation %s is denied by extended ACL check: %w", op, err)
|
|
}
|
|
|
|
var (
|
|
errBearerWrongOwner = errors.New("bearer token must be signed by the container owner")
|
|
errBearerWrongContainer = errors.New("bearer token is created for another container")
|
|
errBearerSignature = errors.New("invalid bearer token signature")
|
|
)
|
|
|
|
// verifyClient verifies if the request for a client operation
|
|
// was signed by a key allowed by (e)ACL rules.
|
|
// Operation must be one of:
|
|
// - 1. ObjectPut;
|
|
// - 2. ObjectGet.
|
|
func (s *Service) verifyClient(req message, cid cidSDK.ID, rawBearer []byte, op acl.Op) error {
|
|
err := verifyMessage(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
isAuthorized, err := s.isAuthorized(req, op)
|
|
if isAuthorized || err != nil {
|
|
return err
|
|
}
|
|
|
|
cnr, err := s.cnrSource.Get(cid)
|
|
if err != nil {
|
|
return fmt.Errorf("can't get container %s: %w", cid, err)
|
|
}
|
|
|
|
eaclOp := eACLOp(op)
|
|
|
|
bt, err := parseBearer(rawBearer, cid, eaclOp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
role, pubKey, err := roleAndPubKeyFromReq(cnr, req, bt)
|
|
if err != nil {
|
|
return fmt.Errorf("can't get request role: %w", err)
|
|
}
|
|
|
|
basicACL := cnr.Value.BasicACL()
|
|
// Basic ACL mask can be unset, if a container operations are performed
|
|
// with strict APE checks only.
|
|
//
|
|
// FIXME(@aarifullin): tree service temporiraly performs APE checks on
|
|
// object verbs, because tree verbs have not been introduced yet.
|
|
if basicACL == 0x0 {
|
|
return s.checkAPE(cnr, cid, op, role, pubKey)
|
|
}
|
|
|
|
if !basicACL.IsOpAllowed(op, role) {
|
|
return basicACLErr(op)
|
|
}
|
|
|
|
if !basicACL.Extendable() {
|
|
return nil
|
|
}
|
|
|
|
var useBearer bool
|
|
if len(rawBearer) != 0 {
|
|
if !basicACL.AllowedBearerRules(op) {
|
|
s.log.Debug(logs.TreeBearerPresentedButNotAllowedByACL,
|
|
zap.String("cid", cid.EncodeToString()),
|
|
zap.Stringer("op", op),
|
|
)
|
|
} else {
|
|
useBearer = true
|
|
}
|
|
}
|
|
|
|
var tb eacl.Table
|
|
signer := req.GetSignature().GetKey()
|
|
if useBearer && !bt.Impersonate() {
|
|
if !bearer.ResolveIssuer(*bt).Equals(cnr.Value.Owner()) {
|
|
return eACLErr(eaclOp, errBearerWrongOwner)
|
|
}
|
|
tb = bt.EACLTable()
|
|
} else {
|
|
tbCore, err := s.eaclSource.GetEACL(cid)
|
|
if err != nil {
|
|
return handleGetEACLError(err)
|
|
}
|
|
tb = *tbCore.Value
|
|
|
|
if useBearer && bt.Impersonate() {
|
|
signer = bt.SigningKeyBytes()
|
|
}
|
|
}
|
|
|
|
return checkEACL(tb, signer, eACLRole(role), eaclOp)
|
|
}
|
|
|
|
// Returns true iff the operation is read-only and request was signed
|
|
// with one of the authorized keys.
|
|
func (s *Service) isAuthorized(req message, op acl.Op) (bool, error) {
|
|
if op != acl.OpObjectGet {
|
|
return false, nil
|
|
}
|
|
|
|
sign := req.GetSignature()
|
|
if sign == nil {
|
|
return false, errors.New("missing signature")
|
|
}
|
|
|
|
key := sign.GetKey()
|
|
for i := range s.authorizedKeys {
|
|
if bytes.Equal(s.authorizedKeys[i], key) {
|
|
return true, nil
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func parseBearer(rawBearer []byte, cid cidSDK.ID, eaclOp eacl.Operation) (*bearer.Token, error) {
|
|
if len(rawBearer) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
bt := new(bearer.Token)
|
|
if err := bt.Unmarshal(rawBearer); err != nil {
|
|
return nil, eACLErr(eaclOp, fmt.Errorf("invalid bearer token: %w", err))
|
|
}
|
|
if !bt.AssertContainer(cid) {
|
|
return nil, eACLErr(eaclOp, errBearerWrongContainer)
|
|
}
|
|
if !bt.VerifySignature() {
|
|
return nil, eACLErr(eaclOp, errBearerSignature)
|
|
}
|
|
return bt, nil
|
|
}
|
|
|
|
func handleGetEACLError(err error) error {
|
|
if client.IsErrEACLNotFound(err) {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("get eACL table: %w", err)
|
|
}
|
|
|
|
func verifyMessage(m message) error {
|
|
binBody, err := m.ReadSignedData(nil)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal request body: %w", err)
|
|
}
|
|
|
|
sig := m.GetSignature()
|
|
|
|
// TODO(@cthulhu-rider): #468 use Signature message from FrostFS API to avoid conversion
|
|
var sigV2 refs.Signature
|
|
sigV2.SetKey(sig.GetKey())
|
|
sigV2.SetSign(sig.GetSign())
|
|
sigV2.SetScheme(refs.ECDSA_SHA512)
|
|
|
|
var sigSDK frostfscrypto.Signature
|
|
if err := sigSDK.ReadFromV2(sigV2); err != nil {
|
|
return fmt.Errorf("can't read signature: %w", err)
|
|
}
|
|
|
|
if !sigSDK.Verify(binBody) {
|
|
return errors.New("invalid signature")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SignMessage uses the provided key and signs any protobuf
|
|
// message that was generated for the TreeService by the
|
|
// protoc-gen-go-frostfs generator. Returns any errors directly.
|
|
func SignMessage(m message, key *ecdsa.PrivateKey) error {
|
|
binBody, err := m.ReadSignedData(nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
keySDK := frostfsecdsa.Signer(*key)
|
|
data, err := keySDK.Sign(binBody)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
rawPub := make([]byte, keySDK.Public().MaxEncodedSize())
|
|
rawPub = rawPub[:keySDK.Public().Encode(rawPub)]
|
|
m.SetSignature(&Signature{
|
|
Key: rawPub,
|
|
Sign: data,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func roleAndPubKeyFromReq(cnr *core.Container, req message, bt *bearer.Token) (acl.Role, *keys.PublicKey, error) {
|
|
role := acl.RoleOthers
|
|
owner := cnr.Value.Owner()
|
|
|
|
rawKey := req.GetSignature().GetKey()
|
|
if bt != nil && bt.Impersonate() {
|
|
rawKey = bt.SigningKeyBytes()
|
|
}
|
|
|
|
pub, err := keys.NewPublicKeyFromBytes(rawKey, elliptic.P256())
|
|
if err != nil {
|
|
return role, nil, fmt.Errorf("invalid public key: %w", err)
|
|
}
|
|
|
|
var reqSigner user.ID
|
|
user.IDFromKey(&reqSigner, (ecdsa.PublicKey)(*pub))
|
|
|
|
if reqSigner.Equals(owner) {
|
|
role = acl.RoleOwner
|
|
}
|
|
|
|
return role, pub, nil
|
|
}
|
|
|
|
func eACLOp(op acl.Op) eacl.Operation {
|
|
switch op {
|
|
case acl.OpObjectGet:
|
|
return eacl.OperationGet
|
|
case acl.OpObjectPut:
|
|
return eacl.OperationPut
|
|
default:
|
|
panic(fmt.Sprintf("unexpected tree service ACL operation: %s", op))
|
|
}
|
|
}
|
|
|
|
func eACLRole(role acl.Role) eacl.Role {
|
|
switch role {
|
|
case acl.RoleOwner:
|
|
return eacl.RoleUser
|
|
case acl.RoleOthers:
|
|
return eacl.RoleOthers
|
|
default:
|
|
panic(fmt.Sprintf("unexpected tree service ACL role: %s", role))
|
|
}
|
|
}
|
|
|
|
var (
|
|
errDENY = errors.New("DENY eACL rule")
|
|
errNoAllowRules = errors.New("not found allowing rules for the request")
|
|
)
|
|
|
|
// checkEACL searches for the eACL rules that could be applied to the request
|
|
// (a tuple of a signer key, his FrostFS role and a request operation).
|
|
// It does not filter the request by the filters of the eACL table since tree
|
|
// requests do not contain any "object" information that could be filtered and,
|
|
// therefore, filtering leads to unexpected results.
|
|
// The code was copied with the minor updates from the SDK repo:
|
|
// https://github.com/nspcc-dev/frostfs-sdk-go/blob/43a57d42dd50dc60465bfd3482f7f12bcfcf3411/eacl/validator.go#L28.
|
|
func checkEACL(tb eacl.Table, signer []byte, role eacl.Role, op eacl.Operation) error {
|
|
for _, record := range tb.Records() {
|
|
// check type of operation
|
|
if record.Operation() != op {
|
|
continue
|
|
}
|
|
|
|
// check target
|
|
if !targetMatches(record, role, signer) {
|
|
continue
|
|
}
|
|
|
|
switch a := record.Action(); a {
|
|
case eacl.ActionAllow:
|
|
return nil
|
|
case eacl.ActionDeny:
|
|
return eACLErr(op, errDENY)
|
|
default:
|
|
return eACLErr(op, fmt.Errorf("unexpected action: %s", a))
|
|
}
|
|
}
|
|
|
|
return eACLErr(op, errNoAllowRules)
|
|
}
|
|
|
|
func targetMatches(rec eacl.Record, role eacl.Role, signer []byte) bool {
|
|
for _, target := range rec.Targets() {
|
|
// check public key match
|
|
if pubs := target.BinaryKeys(); len(pubs) != 0 {
|
|
for _, key := range pubs {
|
|
if bytes.Equal(key, signer) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
continue
|
|
}
|
|
|
|
// check target group match
|
|
if role == target.Role() {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|