forked from TrueCloudLab/frostfs-http-gw
261 lines
6.6 KiB
Go
261 lines
6.6 KiB
Go
package handler
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"git.frostfs.info/TrueCloudLab/frostfs-http-gw/utils"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/checksum"
|
|
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container"
|
|
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/acl"
|
|
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
|
"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"
|
|
)
|
|
|
|
type TestFrostFS struct {
|
|
objects map[string]*object.Object
|
|
containers map[string]*container.Container
|
|
accessList map[string]bool
|
|
key *keys.PrivateKey
|
|
}
|
|
|
|
func NewTestFrostFS(key *keys.PrivateKey) *TestFrostFS {
|
|
return &TestFrostFS{
|
|
objects: make(map[string]*object.Object),
|
|
containers: make(map[string]*container.Container),
|
|
accessList: make(map[string]bool),
|
|
key: key,
|
|
}
|
|
}
|
|
|
|
func (t *TestFrostFS) ContainerID(name string) (*cid.ID, error) {
|
|
for id, cnr := range t.containers {
|
|
if container.Name(*cnr) == name {
|
|
var cnrID cid.ID
|
|
return &cnrID, cnrID.DecodeString(id)
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("not found")
|
|
}
|
|
|
|
func (t *TestFrostFS) SetContainer(cnrID cid.ID, cnr *container.Container) {
|
|
t.containers[cnrID.EncodeToString()] = cnr
|
|
}
|
|
|
|
// AllowUserOperation grants access to object operations.
|
|
// Empty userID and objID means any user and object respectively.
|
|
func (t *TestFrostFS) AllowUserOperation(cnrID cid.ID, userID user.ID, op acl.Op, objID oid.ID) {
|
|
t.accessList[fmt.Sprintf("%s/%s/%s/%s", cnrID, userID, op, objID)] = true
|
|
}
|
|
|
|
func (t *TestFrostFS) Container(_ context.Context, prm PrmContainer) (*container.Container, error) {
|
|
for k, v := range t.containers {
|
|
if k == prm.ContainerID.EncodeToString() {
|
|
return v, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("container not found %s", prm.ContainerID)
|
|
}
|
|
|
|
func (t *TestFrostFS) requestOwner(btoken *bearer.Token) user.ID {
|
|
if btoken != nil {
|
|
return bearer.ResolveIssuer(*btoken)
|
|
}
|
|
|
|
var owner user.ID
|
|
user.IDFromKey(&owner, t.key.PrivateKey.PublicKey)
|
|
return owner
|
|
}
|
|
|
|
func (t *TestFrostFS) ReadObject(_ context.Context, prm PrmObjectRead) (*ObjectPart, error) {
|
|
sAddr := prm.Address.EncodeToString()
|
|
|
|
if obj, ok := t.objects[sAddr]; ok {
|
|
owner := t.requestOwner(prm.BearerToken)
|
|
|
|
if !t.isAllowed(prm.Address.Container(), owner, acl.OpObjectGet, prm.Address.Object()) {
|
|
return nil, ErrAccessDenied
|
|
}
|
|
|
|
payload := obj.Payload()
|
|
|
|
if prm.PayloadRange[0]+prm.PayloadRange[1] > 0 {
|
|
off := prm.PayloadRange[0]
|
|
payload = payload[off : off+prm.PayloadRange[1]]
|
|
obj = nil // GetRange does not return header values
|
|
}
|
|
|
|
return &ObjectPart{
|
|
Head: obj,
|
|
Payload: io.NopCloser(bytes.NewReader(payload)),
|
|
}, nil
|
|
}
|
|
|
|
return nil, fmt.Errorf("%w: %s", &apistatus.ObjectNotFound{}, prm.Address)
|
|
}
|
|
func (t *TestFrostFS) CreateObject(_ context.Context, prm PrmObjectCreate) (oid.ID, error) {
|
|
b := make([]byte, 32)
|
|
if _, err := io.ReadFull(rand.Reader, b); err != nil {
|
|
return oid.ID{}, err
|
|
}
|
|
var id oid.ID
|
|
id.SetSHA256(sha256.Sum256(b))
|
|
prm.Object.SetID(id)
|
|
|
|
attrs := prm.Object.Attributes()
|
|
if prm.ClientCut {
|
|
a := object.NewAttribute()
|
|
a.SetKey("s3-client-cut")
|
|
a.SetValue("true")
|
|
attrs = append(attrs, *a)
|
|
}
|
|
|
|
prm.Object.SetAttributes(attrs...)
|
|
|
|
if prm.Payload != nil {
|
|
all, err := io.ReadAll(prm.Payload)
|
|
if err != nil {
|
|
return oid.ID{}, err
|
|
}
|
|
prm.Object.SetPayload(all)
|
|
prm.Object.SetPayloadSize(uint64(len(all)))
|
|
var hash checksum.Checksum
|
|
checksum.Calculate(&hash, checksum.SHA256, all)
|
|
prm.Object.SetPayloadChecksum(hash)
|
|
}
|
|
|
|
cnrID, _ := prm.Object.ContainerID()
|
|
objID, _ := prm.Object.ID()
|
|
|
|
owner := t.requestOwner(prm.BearerToken)
|
|
|
|
if !t.isAllowed(cnrID, owner, acl.OpObjectPut, objID) {
|
|
return oid.ID{}, ErrAccessDenied
|
|
}
|
|
|
|
addr := newAddress(cnrID, objID)
|
|
t.objects[addr.EncodeToString()] = prm.Object
|
|
return objID, nil
|
|
}
|
|
|
|
type resObjectSearchMock struct {
|
|
res []oid.ID
|
|
}
|
|
|
|
func (r *resObjectSearchMock) Read(buf []oid.ID) (int, error) {
|
|
for i := range buf {
|
|
if i > len(r.res)-1 {
|
|
return len(r.res), io.EOF
|
|
}
|
|
buf[i] = r.res[i]
|
|
}
|
|
|
|
r.res = r.res[len(buf):]
|
|
|
|
return len(buf), nil
|
|
}
|
|
|
|
func (r *resObjectSearchMock) Iterate(f func(oid.ID) bool) error {
|
|
for _, id := range r.res {
|
|
if f(id) {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *resObjectSearchMock) Close() {}
|
|
|
|
func (t *TestFrostFS) SearchObjects(_ context.Context, prm PrmObjectSearch) (ResObjectSearch, error) {
|
|
if !t.isAllowed(prm.Container, t.requestOwner(prm.BearerToken), acl.OpObjectSearch, oid.ID{}) {
|
|
return nil, ErrAccessDenied
|
|
}
|
|
|
|
cidStr := prm.Container.EncodeToString()
|
|
var res []oid.ID
|
|
|
|
if len(prm.Filters) == 1 { // match root filter
|
|
for k, v := range t.objects {
|
|
if strings.Contains(k, cidStr) {
|
|
id, _ := v.ID()
|
|
res = append(res, id)
|
|
}
|
|
}
|
|
return &resObjectSearchMock{res: res}, nil
|
|
}
|
|
|
|
filter := prm.Filters[1]
|
|
if len(prm.Filters) != 2 ||
|
|
filter.Operation() != object.MatchCommonPrefix && filter.Operation() != object.MatchStringEqual {
|
|
return nil, fmt.Errorf("usupported filters")
|
|
}
|
|
|
|
for k, v := range t.objects {
|
|
if strings.Contains(k, cidStr) && isMatched(v.Attributes(), filter) {
|
|
id, _ := v.ID()
|
|
res = append(res, id)
|
|
}
|
|
}
|
|
|
|
return &resObjectSearchMock{res: res}, nil
|
|
}
|
|
|
|
func isMatched(attributes []object.Attribute, filter object.SearchFilter) bool {
|
|
for _, attr := range attributes {
|
|
if attr.Key() == filter.Header() {
|
|
switch filter.Operation() {
|
|
case object.MatchStringEqual:
|
|
return attr.Value() == filter.Value()
|
|
case object.MatchCommonPrefix:
|
|
return strings.HasPrefix(attr.Value(), filter.Value())
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (t *TestFrostFS) GetEpochDurations(context.Context) (*utils.EpochDurations, error) {
|
|
return &utils.EpochDurations{
|
|
CurrentEpoch: 10,
|
|
MsPerBlock: 1000,
|
|
BlockPerEpoch: 100,
|
|
}, nil
|
|
}
|
|
|
|
func (t *TestFrostFS) isAllowed(cnrID cid.ID, userID user.ID, op acl.Op, objID oid.ID) bool {
|
|
keysToCheck := []string{
|
|
fmt.Sprintf("%s/%s/%s/%s", cnrID, userID, op, objID),
|
|
fmt.Sprintf("%s/%s/%s/%s", cnrID, userID, op, oid.ID{}),
|
|
fmt.Sprintf("%s/%s/%s/%s", cnrID, user.ID{}, op, objID),
|
|
fmt.Sprintf("%s/%s/%s/%s", cnrID, user.ID{}, op, oid.ID{}),
|
|
}
|
|
|
|
for _, key := range keysToCheck {
|
|
if t.accessList[key] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func newAddress(cnr cid.ID, obj oid.ID) oid.Address {
|
|
var addr oid.Address
|
|
addr.SetContainer(cnr)
|
|
addr.SetObject(obj)
|
|
return addr
|
|
}
|