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) retrieveObject(addr oid.Address, btoken *bearer.Token) (*object.Object, error) { sAddr := addr.EncodeToString() if obj, ok := t.objects[sAddr]; ok { owner := t.requestOwner(btoken) if !t.isAllowed(addr.Container(), owner, acl.OpObjectGet, addr.Object()) { return nil, ErrAccessDenied } return obj, nil } return nil, fmt.Errorf("%w: %s", &apistatus.ObjectNotFound{}, addr) } func (t *TestFrostFS) HeadObject(_ context.Context, prm PrmObjectHead) (*object.Object, error) { return t.retrieveObject(prm.Address, prm.BearerToken) } func (t *TestFrostFS) GetObject(_ context.Context, prm PrmObjectGet) (*Object, error) { obj, err := t.retrieveObject(prm.Address, prm.BearerToken) if err != nil { return nil, err } return &Object{ Header: *obj, Payload: io.NopCloser(bytes.NewReader(obj.Payload())), }, nil } func (t *TestFrostFS) RangeObject(_ context.Context, prm PrmObjectRange) (io.ReadCloser, error) { obj, err := t.retrieveObject(prm.Address, prm.BearerToken) if err != nil { return nil, err } off := prm.PayloadRange[0] payload := obj.Payload()[off : off+prm.PayloadRange[1]] return io.NopCloser(bytes.NewReader(payload)), nil } 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 (t *TestFrostFS) InitMultiObjectReader(context.Context, PrmInitMultiObjectReader) (io.Reader, error) { return nil, 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 }