forked from TrueCloudLab/frostfs-rest-gw
[#1] Add basic structure and operations
Signed-off-by: Denis Kirillov <denis@nspcc.ru>
This commit is contained in:
parent
eb642eae89
commit
9f752cd756
65 changed files with 11534 additions and 0 deletions
461
cmd/neofs-rest-gw/integration_test.go
Normal file
461
cmd/neofs-rest-gw/integration_test.go
Normal file
|
@ -0,0 +1,461 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha512"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-openapi/loads"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/nspcc-dev/neofs-rest-gw/gen/models"
|
||||
"github.com/nspcc-dev/neofs-rest-gw/gen/restapi"
|
||||
"github.com/nspcc-dev/neofs-rest-gw/gen/restapi/operations"
|
||||
"github.com/nspcc-dev/neofs-rest-gw/handlers"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/container"
|
||||
cid "github.com/nspcc-dev/neofs-sdk-go/container/id"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/eacl"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/object"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/object/address"
|
||||
oid "github.com/nspcc-dev/neofs-sdk-go/object/id"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/policy"
|
||||
"github.com/nspcc-dev/neofs-sdk-go/pool"
|
||||
"github.com/spf13/viper"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
)
|
||||
|
||||
const (
|
||||
devenvPrivateKey = "1dd37fba80fec4e6a6f13fd708d8dcb3b29def768017052f6c930fa1c5d90bbb"
|
||||
testListenAddress = "localhost:8082"
|
||||
testHost = "http://" + testListenAddress
|
||||
testNode = "localhost:8080"
|
||||
|
||||
// XNeofsTokenSignature header contains base64 encoded signature of the token body.
|
||||
XNeofsTokenSignature = "X-Neofs-Token-Signature"
|
||||
// XNeofsTokenSignatureKey header contains hex encoded public key that corresponds the signature of the token body.
|
||||
XNeofsTokenSignatureKey = "X-Neofs-Token-Signature-Key"
|
||||
// XNeofsTokenScope header contains operation scope for auth (bearer) token.
|
||||
// It corresponds to 'object' or 'container' services in neofs.
|
||||
XNeofsTokenScope = "X-Neofs-Token-Scope"
|
||||
)
|
||||
|
||||
func TestIntegration(t *testing.T) {
|
||||
rootCtx := context.Background()
|
||||
aioImage := "nspccdev/neofs-aio-testcontainer:"
|
||||
versions := []string{"0.24.0", "0.25.1", "0.27.5", "latest"}
|
||||
key, err := keys.NewPrivateKeyFromHex(devenvPrivateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, version := range versions {
|
||||
ctx, cancel2 := context.WithCancel(rootCtx)
|
||||
|
||||
aioContainer := createDockerContainer(ctx, t, aioImage+version)
|
||||
cancel := runServer(ctx, t)
|
||||
clientPool := getPool(ctx, t, key)
|
||||
CID, err := createContainer(ctx, t, clientPool)
|
||||
require.NoError(t, err, version)
|
||||
|
||||
t.Run("rest put object "+version, func(t *testing.T) { restObjectPut(ctx, t, clientPool, CID) })
|
||||
t.Run("rest put container"+version, func(t *testing.T) { restContainerPut(ctx, t, clientPool) })
|
||||
t.Run("rest get container"+version, func(t *testing.T) { restContainerGet(ctx, t, clientPool, CID) })
|
||||
|
||||
cancel()
|
||||
err = aioContainer.Terminate(ctx)
|
||||
require.NoError(t, err)
|
||||
cancel2()
|
||||
<-ctx.Done()
|
||||
}
|
||||
}
|
||||
|
||||
func createDockerContainer(ctx context.Context, t *testing.T, image string) testcontainers.Container {
|
||||
req := testcontainers.ContainerRequest{
|
||||
Image: image,
|
||||
WaitingFor: wait.NewLogStrategy("aio container started").WithStartupTimeout(30 * time.Second),
|
||||
Name: "aio",
|
||||
Hostname: "aio",
|
||||
NetworkMode: "host",
|
||||
}
|
||||
aioC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||||
ContainerRequest: req,
|
||||
Started: true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
return aioC
|
||||
}
|
||||
|
||||
func runServer(ctx context.Context, t *testing.T) context.CancelFunc {
|
||||
cancelCtx, cancel := context.WithCancel(ctx)
|
||||
|
||||
v := getDefaultConfig()
|
||||
l := newLogger(v)
|
||||
|
||||
neofsAPI, err := newNeofsAPI(cancelCtx, l, v)
|
||||
require.NoError(t, err)
|
||||
|
||||
swaggerSpec, err := loads.Analyzed(restapi.SwaggerJSON, "")
|
||||
require.NoError(t, err)
|
||||
|
||||
api := operations.NewNeofsRestGwAPI(swaggerSpec)
|
||||
server := restapi.NewServer(api, serverConfig(v))
|
||||
|
||||
server.ConfigureAPI(neofsAPI.Configure)
|
||||
|
||||
go func() {
|
||||
err := server.Serve()
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
return func() {
|
||||
cancel()
|
||||
err := server.Shutdown()
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func getDefaultConfig() *viper.Viper {
|
||||
v := config()
|
||||
v.SetDefault(cfgPeers+".0.address", testNode)
|
||||
v.SetDefault(cfgPeers+".0.weight", 1)
|
||||
v.SetDefault(cfgPeers+".0.priority", 1)
|
||||
v.SetDefault(restapi.FlagListenAddress, testListenAddress)
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func getPool(ctx context.Context, t *testing.T, key *keys.PrivateKey) *pool.Pool {
|
||||
var prm pool.InitParameters
|
||||
prm.AddNode(pool.NewNodeParam(1, testNode, 1))
|
||||
prm.SetKey(&key.PrivateKey)
|
||||
prm.SetHealthcheckTimeout(5 * time.Second)
|
||||
prm.SetNodeDialTimeout(5 * time.Second)
|
||||
|
||||
clientPool, err := pool.NewPool(prm)
|
||||
require.NoError(t, err)
|
||||
err = clientPool.Dial(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
return clientPool
|
||||
}
|
||||
|
||||
func restObjectPut(ctx context.Context, t *testing.T, clientPool *pool.Pool, cnrID *cid.ID) {
|
||||
restrictByEACL(ctx, t, clientPool, cnrID)
|
||||
|
||||
key, err := keys.NewPrivateKeyFromHex(devenvPrivateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
b := models.Bearer{
|
||||
Object: []*models.Record{{
|
||||
Operation: models.NewOperation(models.OperationPUT),
|
||||
Action: models.NewAction(models.ActionALLOW),
|
||||
Filters: []*models.Filter{},
|
||||
Targets: []*models.Target{{
|
||||
Role: models.NewRole(models.RoleOTHERS),
|
||||
Keys: []string{},
|
||||
}},
|
||||
}},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(&b)
|
||||
require.NoError(t, err)
|
||||
|
||||
request0, err := http.NewRequest(http.MethodPost, testHost+"/v1/auth", bytes.NewReader(data))
|
||||
require.NoError(t, err)
|
||||
request0.Header.Add("Content-Type", "application/json")
|
||||
request0.Header.Add(XNeofsTokenScope, string(models.TokenTypeObject))
|
||||
request0.Header.Add(XNeofsTokenSignatureKey, hex.EncodeToString(key.PublicKey().Bytes()))
|
||||
|
||||
httpClient := http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(request0)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
rr, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
fmt.Println(string(rr))
|
||||
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
stokenResp := &models.TokenResponse{}
|
||||
err = json.Unmarshal(rr, stokenResp)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, *stokenResp.Type, models.TokenTypeObject)
|
||||
|
||||
bearerBase64 := stokenResp.Token
|
||||
fmt.Println(*bearerBase64)
|
||||
binaryData, err := base64.StdEncoding.DecodeString(*bearerBase64)
|
||||
require.NoError(t, err)
|
||||
|
||||
signatureData := signData(t, key, binaryData)
|
||||
|
||||
content := "content of file"
|
||||
attrKey, attrValue := "User-Attribute", "user value"
|
||||
|
||||
attributes := map[string]string{
|
||||
object.AttributeFileName: "newFile.txt",
|
||||
attrKey: attrValue,
|
||||
}
|
||||
|
||||
req := operations.PutObjectBody{
|
||||
ContainerID: handlers.NewString(cnrID.String()),
|
||||
FileName: handlers.NewString("newFile.txt"),
|
||||
Payload: base64.StdEncoding.EncodeToString([]byte(content)),
|
||||
}
|
||||
|
||||
body, err := json.Marshal(&req)
|
||||
require.NoError(t, err)
|
||||
|
||||
fmt.Println(base64.StdEncoding.EncodeToString(signatureData))
|
||||
fmt.Println(hex.EncodeToString(key.PublicKey().Bytes()))
|
||||
|
||||
request, err := http.NewRequest(http.MethodPut, testHost+"/v1/objects", bytes.NewReader(body))
|
||||
require.NoError(t, err)
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
request.Header.Add(XNeofsTokenSignature, base64.StdEncoding.EncodeToString(signatureData))
|
||||
request.Header.Add("Authorization", "Bearer "+*bearerBase64)
|
||||
request.Header.Add(XNeofsTokenSignatureKey, hex.EncodeToString(key.PublicKey().Bytes()))
|
||||
request.Header.Add("X-Attribute-"+attrKey, attrValue)
|
||||
|
||||
resp2, err := httpClient.Do(request)
|
||||
require.NoError(t, err)
|
||||
defer resp2.Body.Close()
|
||||
|
||||
rr2, err := io.ReadAll(resp2.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
fmt.Println(string(rr2))
|
||||
require.Equal(t, http.StatusOK, resp2.StatusCode)
|
||||
|
||||
addr := &operations.PutObjectOKBody{}
|
||||
err = json.Unmarshal(rr2, addr)
|
||||
require.NoError(t, err)
|
||||
|
||||
var CID cid.ID
|
||||
err = CID.Parse(*addr.ContainerID)
|
||||
require.NoError(t, err)
|
||||
|
||||
id := oid.NewID()
|
||||
err = id.Parse(*addr.ObjectID)
|
||||
require.NoError(t, err)
|
||||
|
||||
objectAddress := address.NewAddress()
|
||||
objectAddress.SetContainerID(&CID)
|
||||
objectAddress.SetObjectID(id)
|
||||
|
||||
payload := bytes.NewBuffer(nil)
|
||||
|
||||
var prm pool.PrmObjectGet
|
||||
prm.SetAddress(*objectAddress)
|
||||
|
||||
res, err := clientPool.GetObject(ctx, prm)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = io.Copy(payload, res.Payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, content, payload.String())
|
||||
|
||||
for _, attribute := range res.Header.Attributes() {
|
||||
require.Equal(t, attributes[attribute.Key()], attribute.Value(), attribute.Key())
|
||||
}
|
||||
}
|
||||
|
||||
func restContainerGet(ctx context.Context, t *testing.T, clientPool *pool.Pool, cnrID *cid.ID) {
|
||||
httpClient := http.Client{Timeout: 5 * time.Second}
|
||||
request, err := http.NewRequest(http.MethodGet, testHost+"/v1/containers/"+cnrID.String(), nil)
|
||||
require.NoError(t, err)
|
||||
request = request.WithContext(ctx)
|
||||
|
||||
resp, err := httpClient.Do(request)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
cnrInfo := &models.ContainerInfo{}
|
||||
err = json.NewDecoder(resp.Body).Decode(cnrInfo)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, cnrID.String(), cnrInfo.ContainerID)
|
||||
require.Equal(t, clientPool.OwnerID().String(), cnrInfo.OwnerID)
|
||||
}
|
||||
|
||||
func signData(t *testing.T, key *keys.PrivateKey, data []byte) []byte {
|
||||
h := sha512.Sum512(data)
|
||||
x, y, err := ecdsa.Sign(rand.Reader, &key.PrivateKey, h[:])
|
||||
require.NoError(t, err)
|
||||
return elliptic.Marshal(elliptic.P256(), x, y)
|
||||
}
|
||||
|
||||
func restContainerPut(ctx context.Context, t *testing.T, clientPool *pool.Pool) {
|
||||
key, err := keys.NewPrivateKeyFromHex(devenvPrivateKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
b := models.Bearer{
|
||||
Container: &models.Rule{
|
||||
Verb: models.NewVerb(models.VerbPUT),
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(&b)
|
||||
require.NoError(t, err)
|
||||
|
||||
request0, err := http.NewRequest(http.MethodPost, testHost+"/v1/auth", bytes.NewReader(data))
|
||||
require.NoError(t, err)
|
||||
request0.Header.Add("Content-Type", "application/json")
|
||||
request0.Header.Add(XNeofsTokenScope, "container")
|
||||
request0.Header.Add(XNeofsTokenSignatureKey, hex.EncodeToString(key.PublicKey().Bytes()))
|
||||
|
||||
httpClient := http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(request0)
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
|
||||
rr, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
fmt.Println(string(rr))
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
stokenResp := &models.TokenResponse{}
|
||||
err = json.Unmarshal(rr, stokenResp)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, *stokenResp.Type, models.TokenTypeContainer)
|
||||
|
||||
bearerBase64 := stokenResp.Token
|
||||
binaryData, err := base64.StdEncoding.DecodeString(*bearerBase64)
|
||||
require.NoError(t, err)
|
||||
|
||||
signatureData := signData(t, key, binaryData)
|
||||
|
||||
attrKey, attrValue := "User-Attribute", "user value"
|
||||
|
||||
userAttributes := map[string]string{
|
||||
attrKey: attrValue,
|
||||
}
|
||||
|
||||
req := operations.PutContainerBody{
|
||||
ContainerName: handlers.NewString("cnr"),
|
||||
}
|
||||
|
||||
body, err := json.Marshal(&req)
|
||||
require.NoError(t, err)
|
||||
|
||||
fmt.Println(base64.StdEncoding.EncodeToString(signatureData))
|
||||
fmt.Println(hex.EncodeToString(key.PublicKey().Bytes()))
|
||||
|
||||
request, err := http.NewRequest(http.MethodPut, testHost+"/v1/containers", bytes.NewReader(body))
|
||||
require.NoError(t, err)
|
||||
request.Header.Add("Content-Type", "application/json")
|
||||
request.Header.Add(XNeofsTokenSignature, base64.StdEncoding.EncodeToString(signatureData))
|
||||
request.Header.Add("Authorization", "Bearer "+*bearerBase64)
|
||||
request.Header.Add(XNeofsTokenSignatureKey, hex.EncodeToString(key.PublicKey().Bytes()))
|
||||
request.Header.Add("X-Attribute-"+attrKey, attrValue)
|
||||
|
||||
resp2, err := httpClient.Do(request)
|
||||
require.NoError(t, err)
|
||||
defer resp2.Body.Close()
|
||||
|
||||
body, err = io.ReadAll(resp2.Body)
|
||||
require.NoError(t, err)
|
||||
fmt.Println(string(body))
|
||||
|
||||
require.Equal(t, http.StatusOK, resp2.StatusCode)
|
||||
|
||||
addr := &operations.PutContainerOKBody{}
|
||||
err = json.Unmarshal(body, addr)
|
||||
require.NoError(t, err)
|
||||
|
||||
var CID cid.ID
|
||||
err = CID.Parse(*addr.ContainerID)
|
||||
require.NoError(t, err)
|
||||
fmt.Println(CID.String())
|
||||
|
||||
var prm pool.PrmContainerGet
|
||||
prm.SetContainerID(CID)
|
||||
|
||||
cnr, err := clientPool.GetContainer(ctx, prm)
|
||||
require.NoError(t, err)
|
||||
|
||||
cnrAttr := make(map[string]string, len(cnr.Attributes()))
|
||||
for _, attribute := range cnr.Attributes() {
|
||||
cnrAttr[attribute.Key()] = attribute.Value()
|
||||
}
|
||||
|
||||
for key, val := range userAttributes {
|
||||
require.Equal(t, val, cnrAttr[key])
|
||||
}
|
||||
}
|
||||
|
||||
func createContainer(ctx context.Context, t *testing.T, clientPool *pool.Pool) (*cid.ID, error) {
|
||||
pp, err := policy.Parse("REP 1")
|
||||
require.NoError(t, err)
|
||||
|
||||
cnr := container.New(
|
||||
container.WithPolicy(pp),
|
||||
container.WithCustomBasicACL(0x0FFFFFFF),
|
||||
container.WithAttribute(container.AttributeName, "friendlyName"),
|
||||
container.WithAttribute(container.AttributeTimestamp, strconv.FormatInt(time.Now().Unix(), 10)))
|
||||
cnr.SetOwnerID(clientPool.OwnerID())
|
||||
|
||||
var waitPrm pool.WaitParams
|
||||
waitPrm.SetPollInterval(3 * time.Second)
|
||||
waitPrm.SetTimeout(15 * time.Second)
|
||||
|
||||
var prm pool.PrmContainerPut
|
||||
prm.SetContainer(*cnr)
|
||||
prm.SetWaitParams(waitPrm)
|
||||
|
||||
CID, err := clientPool.PutContainer(ctx, prm)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fmt.Println(CID.String())
|
||||
|
||||
return CID, err
|
||||
}
|
||||
|
||||
func restrictByEACL(ctx context.Context, t *testing.T, clientPool *pool.Pool, cnrID *cid.ID) {
|
||||
table := new(eacl.Table)
|
||||
table.SetCID(cnrID)
|
||||
|
||||
for op := eacl.OperationGet; op <= eacl.OperationRangeHash; op++ {
|
||||
record := new(eacl.Record)
|
||||
record.SetOperation(op)
|
||||
record.SetAction(eacl.ActionDeny)
|
||||
target := new(eacl.Target)
|
||||
target.SetRole(eacl.RoleOthers)
|
||||
record.SetTargets(*target)
|
||||
table.AddRecord(record)
|
||||
}
|
||||
|
||||
var waitPrm pool.WaitParams
|
||||
waitPrm.SetPollInterval(3 * time.Second)
|
||||
waitPrm.SetTimeout(15 * time.Second)
|
||||
|
||||
var prm pool.PrmContainerSetEACL
|
||||
prm.SetTable(*table)
|
||||
prm.SetWaitParams(waitPrm)
|
||||
|
||||
err := clientPool.SetEACL(ctx, prm)
|
||||
require.NoError(t, err)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue