This repository has been archived on 2024-09-11. You can view files and clone it, but cannot push or open issues or pull requests.
frostfs-rest-gw/cmd/neofs-rest-gw/integration_test.go

684 lines
20 KiB
Go
Raw Normal View History

package main
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha512"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"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"
containerName = "test-container"
// XBearerSignature header contains base64 encoded signature of the token body.
XBearerSignature = "X-Bearer-Signature"
// XBearerSignatureKey header contains hex encoded public key that corresponds the signature of the token body.
XBearerSignatureKey = "X-Bearer-Signature-Key"
// XBearerScope header contains operation scope for auth (bearer) token.
// It corresponds to 'object' or 'container' services in neofs.
XBearerScope = "X-Bearer-Scope"
)
func TestIntegration(t *testing.T) {
rootCtx := context.Background()
aioImage := "nspccdev/neofs-aio-testcontainer:"
versions := []string{
//"0.24.0",
//"0.25.1",
//"0.26.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)
cnrID := createContainer(ctx, t, clientPool, containerName)
restrictByEACL(ctx, t, clientPool, cnrID)
t.Run("rest put object "+version, func(t *testing.T) { restObjectPut(ctx, t, clientPool, cnrID) })
t.Run("rest get object "+version, func(t *testing.T) { restObjectGet(ctx, t, clientPool, cnrID) })
t.Run("rest delete object "+version, func(t *testing.T) { restObjectDelete(ctx, t, clientPool, cnrID) })
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, cnrID) })
t.Run("rest delete container"+version, func(t *testing.T) { restContainerDelete(ctx, t, clientPool) })
t.Run("rest put container eacl "+version, func(t *testing.T) { restContainerEACLPut(ctx, t, clientPool) })
t.Run("rest get container eacl "+version, func(t *testing.T) { restContainerEACLGet(ctx, t, clientPool, cnrID) })
t.Run("rest list containers "+version, func(t *testing.T) { restContainerList(ctx, t, clientPool, cnrID) })
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 defaultHTTPClient() *http.Client {
return &http.Client{Timeout: 60 * time.Second}
}
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)
v.SetDefault(restapi.FlagWriteTimeout, 60*time.Second)
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) {
bearer := &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{},
}},
}},
}
httpClient := defaultHTTPClient()
bearerToken := makeAuthObjectTokenRequest(ctx, t, bearer, httpClient)
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)
request, err := http.NewRequest(http.MethodPut, testHost+"/v1/objects", bytes.NewReader(body))
require.NoError(t, err)
prepareCommonHeaders(request.Header, bearerToken)
request.Header.Add("X-Attribute-"+attrKey, attrValue)
addr := &operations.PutObjectOKBody{}
doRequest(t, httpClient, request, http.StatusOK, addr)
var CID cid.ID
err = CID.Parse(*addr.ContainerID)
require.NoError(t, err)
var id oid.ID
err = id.Parse(*addr.ObjectID)
require.NoError(t, err)
objectAddress := address.NewAddress()
objectAddress.SetContainerID(&CID)
objectAddress.SetObjectID(&id)
var prm pool.PrmObjectGet
prm.SetAddress(*objectAddress)
res, err := clientPool.GetObject(ctx, prm)
require.NoError(t, err)
payload := bytes.NewBuffer(nil)
_, 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 restObjectGet(ctx context.Context, t *testing.T, p *pool.Pool, cnrID *cid.ID) {
attributes := map[string]string{
object.AttributeFileName: "get-obj-name",
"user-attribute": "user value",
}
objID := createObject(ctx, t, p, cnrID, attributes, []byte("some content"))
bearer := &models.Bearer{
Object: []*models.Record{{
Operation: models.NewOperation(models.OperationGET),
Action: models.NewAction(models.ActionALLOW),
Filters: []*models.Filter{},
Targets: []*models.Target{{
Role: models.NewRole(models.RoleOTHERS),
Keys: []string{},
}},
}},
}
httpClient := defaultHTTPClient()
bearerToken := makeAuthObjectTokenRequest(ctx, t, bearer, httpClient)
request, err := http.NewRequest(http.MethodGet, testHost+"/v1/objects/"+cnrID.String()+"/"+objID.String(), nil)
require.NoError(t, err)
prepareCommonHeaders(request.Header, bearerToken)
objInfo := &models.ObjectInfo{}
doRequest(t, httpClient, request, http.StatusOK, objInfo)
require.Equal(t, cnrID.String(), *objInfo.ContainerID)
require.Equal(t, objID.String(), *objInfo.ObjectID)
require.Equal(t, p.OwnerID().String(), *objInfo.OwnerID)
require.Equal(t, len(attributes), len(objInfo.Attributes))
for _, attr := range objInfo.Attributes {
require.Equal(t, attributes[*attr.Key], *attr.Value)
}
}
func restObjectDelete(ctx context.Context, t *testing.T, p *pool.Pool, cnrID *cid.ID) {
objID := createObject(ctx, t, p, cnrID, nil, []byte("some content"))
bearer := &models.Bearer{
Object: []*models.Record{{
Operation: models.NewOperation(models.OperationDELETE),
Action: models.NewAction(models.ActionALLOW),
Filters: []*models.Filter{},
Targets: []*models.Target{{
Role: models.NewRole(models.RoleOTHERS),
Keys: []string{},
}},
}},
}
httpClient := defaultHTTPClient()
bearerToken := makeAuthObjectTokenRequest(ctx, t, bearer, httpClient)
request, err := http.NewRequest(http.MethodDelete, testHost+"/v1/objects/"+cnrID.String()+"/"+objID.String(), nil)
require.NoError(t, err)
prepareCommonHeaders(request.Header, bearerToken)
doRequest(t, httpClient, request, http.StatusNoContent, nil)
var addr address.Address
addr.SetContainerID(cnrID)
addr.SetObjectID(objID)
var prm pool.PrmObjectHead
prm.SetAddress(addr)
_, err = p.HeadObject(ctx, prm)
require.Error(t, err)
}
func doRequest(t *testing.T, httpClient *http.Client, request *http.Request, expectedCode int, model interface{}) {
resp, err := httpClient.Do(request)
require.NoError(t, err)
defer func() {
err := resp.Body.Close()
require.NoError(t, err)
}()
respBody, err := io.ReadAll(resp.Body)
require.NoError(t, err)
if expectedCode != resp.StatusCode {
fmt.Println("resp", string(respBody))
}
require.Equal(t, expectedCode, resp.StatusCode)
if model == nil {
return
}
err = json.Unmarshal(respBody, model)
require.NoError(t, err)
}
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)
cnrInfo := &models.ContainerInfo{}
doRequest(t, httpClient, request, http.StatusOK, cnrInfo)
require.Equal(t, cnrID.String(), *cnrInfo.ContainerID)
require.Equal(t, clientPool.OwnerID().String(), *cnrInfo.OwnerID)
}
func restContainerDelete(ctx context.Context, t *testing.T, clientPool *pool.Pool) {
cnrID := createContainer(ctx, t, clientPool, "for-delete")
bearer := &models.Bearer{
Container: &models.Rule{
Verb: models.NewVerb(models.VerbDELETE),
},
}
httpClient := defaultHTTPClient()
bearerToken := makeAuthContainerTokenRequest(ctx, t, bearer, httpClient)
request, err := http.NewRequest(http.MethodDelete, testHost+"/v1/containers/"+cnrID.String(), nil)
require.NoError(t, err)
request = request.WithContext(ctx)
prepareCommonHeaders(request.Header, bearerToken)
doRequest(t, httpClient, request, http.StatusNoContent, nil)
var prm pool.PrmContainerGet
prm.SetContainerID(*cnrID)
_, err = clientPool.GetContainer(ctx, prm)
require.Error(t, err)
require.Contains(t, err.Error(), "not found")
}
func restContainerEACLPut(ctx context.Context, t *testing.T, clientPool *pool.Pool) {
cnrID := createContainer(ctx, t, clientPool, "for-eacl-put")
httpClient := &http.Client{Timeout: 60 * time.Second}
bearer := &models.Bearer{
Container: &models.Rule{
Verb: models.NewVerb(models.VerbSETEACL),
},
}
bearerToken := makeAuthContainerTokenRequest(ctx, t, bearer, httpClient)
req := models.Eacl{
Records: []*models.Record{{
Action: models.NewAction(models.ActionDENY),
Filters: []*models.Filter{},
Operation: models.NewOperation(models.OperationDELETE),
Targets: []*models.Target{{
Keys: []string{},
Role: models.NewRole(models.RoleOTHERS),
}},
}},
}
body, err := json.Marshal(&req)
require.NoError(t, err)
request, err := http.NewRequest(http.MethodPut, testHost+"/v1/containers/"+cnrID.String()+"/eacl", bytes.NewReader(body))
require.NoError(t, err)
request = request.WithContext(ctx)
prepareCommonHeaders(request.Header, bearerToken)
doRequest(t, httpClient, request, http.StatusOK, nil)
var prm pool.PrmContainerEACL
prm.SetContainerID(*cnrID)
table, err := clientPool.GetEACL(ctx, prm)
require.NoError(t, err)
expectedTable, err := handlers.ToNativeTable(req.Records)
require.NoError(t, err)
expectedTable.SetCID(cnrID)
require.True(t, eacl.EqualTables(*expectedTable, *table))
}
func restContainerEACLGet(ctx context.Context, t *testing.T, p *pool.Pool, cnrID *cid.ID) {
var prm pool.PrmContainerEACL
prm.SetContainerID(*cnrID)
expectedTable, err := p.GetEACL(ctx, prm)
require.NoError(t, err)
httpClient := &http.Client{Timeout: 60 * time.Second}
request, err := http.NewRequest(http.MethodGet, testHost+"/v1/containers/"+cnrID.String()+"/eacl", nil)
require.NoError(t, err)
request = request.WithContext(ctx)
responseTable := &models.Eacl{}
doRequest(t, httpClient, request, http.StatusOK, responseTable)
require.Equal(t, cnrID.String(), responseTable.ContainerID)
actualTable, err := handlers.ToNativeTable(responseTable.Records)
require.NoError(t, err)
actualTable.SetCID(cnrID)
require.True(t, eacl.EqualTables(*expectedTable, *actualTable))
}
func restContainerList(ctx context.Context, t *testing.T, p *pool.Pool, cnrID *cid.ID) {
var prm pool.PrmContainerList
prm.SetOwnerID(*p.OwnerID())
ids, err := p.ListContainers(ctx, prm)
require.NoError(t, err)
httpClient := defaultHTTPClient()
query := make(url.Values)
query.Add("ownerId", p.OwnerID().String())
request, err := http.NewRequest(http.MethodGet, testHost+"/v1/containers?"+query.Encode(), nil)
require.NoError(t, err)
request = request.WithContext(ctx)
list := &models.ContainerList{}
doRequest(t, httpClient, request, http.StatusOK, list)
require.Equal(t, len(ids), int(*list.Size))
expected := &models.ContainerBaseInfo{
ContainerID: handlers.NewString(cnrID.String()),
Name: containerName,
}
require.Contains(t, list.Containers, expected)
}
func makeAuthContainerTokenRequest(ctx context.Context, t *testing.T, bearer *models.Bearer, httpClient *http.Client) *handlers.BearerToken {
return makeAuthTokenRequest(ctx, t, bearer, httpClient, models.TokenTypeContainer)
}
func makeAuthObjectTokenRequest(ctx context.Context, t *testing.T, bearer *models.Bearer, httpClient *http.Client) *handlers.BearerToken {
return makeAuthTokenRequest(ctx, t, bearer, httpClient, models.TokenTypeObject)
}
func makeAuthTokenRequest(ctx context.Context, t *testing.T, bearer *models.Bearer, httpClient *http.Client, tokenType models.TokenType) *handlers.BearerToken {
key, err := keys.NewPrivateKeyFromHex(devenvPrivateKey)
require.NoError(t, err)
hexPubKey := hex.EncodeToString(key.PublicKey().Bytes())
data, err := json.Marshal(bearer)
require.NoError(t, err)
request, err := http.NewRequest(http.MethodPost, testHost+"/v1/auth", bytes.NewReader(data))
require.NoError(t, err)
request = request.WithContext(ctx)
request.Header.Add("Content-Type", "application/json")
request.Header.Add(XBearerScope, string(tokenType))
request.Header.Add(XBearerSignatureKey, hexPubKey)
resp, err := httpClient.Do(request)
require.NoError(t, err)
defer func() {
err := resp.Body.Close()
require.NoError(t, err)
}()
rr, err := io.ReadAll(resp.Body)
require.NoError(t, err)
if resp.StatusCode != http.StatusOK {
fmt.Println("auth response", 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, tokenType)
binaryData, err := base64.StdEncoding.DecodeString(*stokenResp.Token)
require.NoError(t, err)
signatureData := signData(t, key, binaryData)
signature := base64.StdEncoding.EncodeToString(signatureData)
bt := handlers.BearerToken{
Token: *stokenResp.Token,
Signature: signature,
Key: hexPubKey,
}
fmt.Printf("container token:\n%+v\n", bt)
return &bt
}
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) {
bearer := &models.Bearer{
Container: &models.Rule{
Verb: models.NewVerb(models.VerbPUT),
},
}
httpClient := &http.Client{Timeout: 30 * time.Second}
bearerToken := makeAuthContainerTokenRequest(ctx, t, bearer, httpClient)
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)
reqURL, err := url.Parse(testHost + "/v1/containers")
require.NoError(t, err)
query := reqURL.Query()
query.Add("skip-native-name", "true")
reqURL.RawQuery = query.Encode()
request, err := http.NewRequest(http.MethodPut, reqURL.String(), bytes.NewReader(body))
require.NoError(t, err)
prepareCommonHeaders(request.Header, bearerToken)
request.Header.Add("X-Attribute-"+attrKey, attrValue)
addr := &operations.PutContainerOKBody{}
doRequest(t, httpClient, request, http.StatusOK, addr)
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 prepareCommonHeaders(header http.Header, bearerToken *handlers.BearerToken) {
header.Add("Content-Type", "application/json")
header.Add(XBearerSignature, bearerToken.Signature)
header.Add("Authorization", "Bearer "+bearerToken.Token)
header.Add(XBearerSignatureKey, bearerToken.Key)
}
func createContainer(ctx context.Context, t *testing.T, clientPool *pool.Pool, name string) *cid.ID {
pp, err := policy.Parse("REP 1")
require.NoError(t, err)
cnr := container.New(
container.WithPolicy(pp),
container.WithCustomBasicACL(0x0FFFFFFF),
container.WithAttribute(container.AttributeName, name),
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)
require.NoError(t, err)
return CID
}
func createObject(ctx context.Context, t *testing.T, p *pool.Pool, cnrID *cid.ID, headers map[string]string, payload []byte) *oid.ID {
attributes := make([]object.Attribute, 0, len(headers))
for key, val := range headers {
attr := object.NewAttribute()
attr.SetKey(key)
attr.SetValue(val)
attributes = append(attributes, *attr)
}
obj := object.New()
obj.SetOwnerID(p.OwnerID())
obj.SetContainerID(cnrID)
obj.SetAttributes(attributes...)
obj.SetPayload(payload)
var prm pool.PrmObjectPut
prm.SetHeader(*obj)
objID, err := p.PutObject(ctx, prm)
require.NoError(t, err)
return objID
}
func restrictByEACL(ctx context.Context, t *testing.T, clientPool *pool.Pool, cnrID *cid.ID) *eacl.Table {
table := eacl.NewTable()
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)
return table
}