package main import ( "bytes" "context" "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-rest-gw/internal/util" "github.com/nspcc-dev/neofs-sdk-go/bearer" "github.com/nspcc-dev/neofs-sdk-go/container" "github.com/nspcc-dev/neofs-sdk-go/container/acl" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" "github.com/nspcc-dev/neofs-sdk-go/eacl" "github.com/nspcc-dev/neofs-sdk-go/netmap" "github.com/nspcc-dev/neofs-sdk-go/object" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" "github.com/nspcc-dev/neofs-sdk-go/pool" "github.com/nspcc-dev/neofs-sdk-go/user" "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 testContainerNode = "localhost:8080" testLocalNode = "s01.neofs.devenv:8080" containerName = "test-container" localVersion = "local" walletConnectQuery = "walletConnect" fullBearerQuery = "fullBearer" // 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" // XBearerOwnerID header contains owner id (wallet address) that corresponds the signature of the token body. XBearerOwnerID = "X-Bearer-Owner-Id" // XBearerForAllUsers header specifies if we want all users can use token or only specific gate. XBearerForAllUsers = "X-Bearer-For-All-Users" // tests configuration. useWalletConnect = true useLocalEnvironment = false ) func TestIntegration(t *testing.T) { ctx := context.Background() key, err := keys.NewPrivateKeyFromHex(devenvPrivateKey) require.NoError(t, err) if useLocalEnvironment { runLocalTests(ctx, t, key) } else { runTestInContainer(ctx, t, key) } } func runLocalTests(ctx context.Context, t *testing.T, key *keys.PrivateKey) { runTests(ctx, t, key, localVersion) } func runTestInContainer(rootCtx context.Context, t *testing.T, key *keys.PrivateKey) { aioImage := "nspccdev/neofs-aio-testcontainer:" versions := []string{ "0.29.0", "0.30.0", "0.32.0", "latest", } for _, version := range versions { ctx, cancel := context.WithCancel(rootCtx) aioContainer := createDockerContainer(ctx, t, aioImage+version) runTests(ctx, t, key, version) err := aioContainer.Terminate(ctx) require.NoError(t, err) cancel() <-ctx.Done() } } func runTests(ctx context.Context, t *testing.T, key *keys.PrivateKey, version string) { node := testContainerNode if version == localVersion { node = testLocalNode } cancel := runServer(ctx, t, node) defer cancel() var owner user.ID user.IDFromKey(&owner, key.PrivateKey.PublicKey) clientPool := getPool(ctx, t, key, node) cnrID := createContainer(ctx, t, clientPool, owner, containerName) restrictByEACL(ctx, t, clientPool, cnrID) t.Run("rest auth several tokens "+version, func(t *testing.T) { authTokens(ctx, t) }) t.Run("rest check mix tokens up "+version, func(t *testing.T) { mixTokens(ctx, t, cnrID) }) t.Run("rest form full binary bearer "+version, func(t *testing.T) { formFullBinaryBearer(ctx, t) }) 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, &owner, cnrID) }) t.Run("rest get object unauthenticated "+version, func(t *testing.T) { restObjectGetUnauthenticated(ctx, t, clientPool, &owner, cnrID) }) t.Run("rest get object full bearer "+version, func(t *testing.T) { restObjectGetFullBearer(ctx, t, clientPool, &owner, cnrID) }) t.Run("rest delete object "+version, func(t *testing.T) { restObjectDelete(ctx, t, clientPool, &owner, cnrID) }) t.Run("rest search objects "+version, func(t *testing.T) { restObjectsSearch(ctx, t, clientPool, &owner, cnrID) }) t.Run("rest put container invalid "+version, func(t *testing.T) { restContainerPutInvalid(ctx, t) }) 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, owner, cnrID) }) t.Run("rest delete container "+version, func(t *testing.T) { restContainerDelete(ctx, t, clientPool, owner) }) t.Run("rest put container eacl "+version, func(t *testing.T) { restContainerEACLPut(ctx, t, clientPool, owner) }) 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, owner, cnrID) }) } 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, node string) context.CancelFunc { cancelCtx, cancel := context.WithCancel(ctx) v := getDefaultConfig(node) 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(node string) *viper.Viper { v := config() v.SetDefault(cfgPeers+".0.address", node) 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, node string) *pool.Pool { var prm pool.InitParameters prm.AddNode(pool.NewNodeParam(1, node, 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 getRestrictBearerRecords() []*models.Record { return []*models.Record{ formRestrictRecord(models.OperationGET), formRestrictRecord(models.OperationHEAD), formRestrictRecord(models.OperationPUT), formRestrictRecord(models.OperationDELETE), formRestrictRecord(models.OperationSEARCH), formRestrictRecord(models.OperationRANGE), formRestrictRecord(models.OperationRANGEHASH), } } func formRestrictRecord(op models.Operation) *models.Record { return &models.Record{ Operation: models.NewOperation(op), Action: models.NewAction(models.ActionDENY), Filters: []*models.Filter{}, Targets: []*models.Target{{ Role: models.NewRole(models.RoleOTHERS), Keys: []string{}, }}} } func authTokens(ctx context.Context, t *testing.T) { bearers := []*models.Bearer{ { Name: "all-object", 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{}, }}, }}, }, { Name: "put-container", Container: &models.Rule{ Verb: models.NewVerb(models.VerbPUT), }, }, { Name: "seteacl-container", Container: &models.Rule{ Verb: models.NewVerb(models.VerbSETEACL), }, }, { Name: "delete-container", Container: &models.Rule{ Verb: models.NewVerb(models.VerbDELETE), }, }, } httpClient := defaultHTTPClient() makeAuthTokenRequest(ctx, t, bearers, httpClient, false) } func mixTokens(ctx context.Context, t *testing.T, cnrID cid.ID) { bearers := []*models.Bearer{ { Name: "all-object", 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{}, }}, }}, }, { Name: "put-container", Container: &models.Rule{ Verb: models.NewVerb(models.VerbPUT), }, }, { Name: "seteacl-container", Container: &models.Rule{ Verb: models.NewVerb(models.VerbSETEACL), }, }, } httpClient := defaultHTTPClient() tokens := makeAuthTokenRequest(ctx, t, bearers, httpClient, false) objectToken := tokens[0] containerPutToken := tokens[1] containerSetEACLToken := tokens[2] // check reject object token when container tokens is required checkPutContainerWithError(t, httpClient, objectToken) // check reject wrong verb container token checkPutContainerWithError(t, httpClient, containerSetEACLToken) // check reject wrong verb container token checkDeleteContainerWithError(t, httpClient, cnrID, containerSetEACLToken) // check reject wrong verb container token checkSetEACLContainerWithError(t, httpClient, cnrID, containerPutToken) // check reject container token when object tokens is required checkPutObjectWithError(t, httpClient, cnrID, containerSetEACLToken) } func formFullBinaryBearer(ctx context.Context, t *testing.T) { bearers := []*models.Bearer{ { Name: "all-object", 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{}, }}, }}, }, { Name: "put-container", Container: &models.Rule{ Verb: models.NewVerb(models.VerbPUT), }, }, } httpClient := defaultHTTPClient() tokens := makeAuthTokenRequest(ctx, t, bearers, httpClient, false) objectToken := tokens[0] containerPutToken := tokens[1] query := make(url.Values) query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) // check that container token isn't valid request, err := http.NewRequest(http.MethodGet, testHost+"/v1/auth/bearer?"+query.Encode(), nil) require.NoError(t, err) prepareCommonHeaders(request.Header, containerPutToken) checkGWErrorResponse(t, httpClient, request) // check that object bearer token is valid request, err = http.NewRequest(http.MethodGet, testHost+"/v1/auth/bearer?"+query.Encode(), nil) require.NoError(t, err) prepareCommonHeaders(request.Header, objectToken) resp := &models.BinaryBearer{} doRequest(t, httpClient, request, http.StatusOK, resp) actualTokenRaw, err := base64.StdEncoding.DecodeString(*resp.Token) require.NoError(t, err) var actualToken bearer.Token err = actualToken.Unmarshal(actualTokenRaw) require.NoError(t, err) require.True(t, actualToken.VerifySignature()) require.Len(t, actualToken.EACLTable().Records(), 1) actualRecord := actualToken.EACLTable().Records()[0] require.Equal(t, eacl.OperationPut, actualRecord.Operation()) require.Equal(t, eacl.ActionAllow, actualRecord.Action()) require.Empty(t, actualRecord.Filters()) require.Len(t, actualRecord.Targets(), 1) actualTarget := actualRecord.Targets()[0] require.Empty(t, actualTarget.BinaryKeys()) require.Equal(t, eacl.RoleOthers, actualTarget.Role()) } func checkPutContainerWithError(t *testing.T, httpClient *http.Client, token *handlers.BearerToken) { reqURL, err := url.Parse(testHost + "/v1/containers") require.NoError(t, err) body, err := json.Marshal(&models.ContainerPutInfo{ContainerName: "container"}) require.NoError(t, err) request, err := http.NewRequest(http.MethodPut, reqURL.String(), bytes.NewReader(body)) require.NoError(t, err) prepareCommonHeaders(request.Header, token) checkGWErrorResponse(t, httpClient, request) } func checkDeleteContainerWithError(t *testing.T, httpClient *http.Client, cnrID cid.ID, token *handlers.BearerToken) { reqURL, err := url.Parse(testHost + "/v1/containers/" + cnrID.EncodeToString()) require.NoError(t, err) request, err := http.NewRequest(http.MethodDelete, reqURL.String(), nil) require.NoError(t, err) prepareCommonHeaders(request.Header, token) checkGWErrorResponse(t, httpClient, request) } func checkSetEACLContainerWithError(t *testing.T, httpClient *http.Client, cnrID cid.ID, token *handlers.BearerToken) { req := models.Eacl{Records: []*models.Record{}} body, err := json.Marshal(&req) require.NoError(t, err) request, err := http.NewRequest(http.MethodPut, testHost+"/v1/containers/"+cnrID.EncodeToString()+"/eacl", bytes.NewReader(body)) require.NoError(t, err) prepareCommonHeaders(request.Header, token) checkGWErrorResponse(t, httpClient, request) } func checkPutObjectWithError(t *testing.T, httpClient *http.Client, cnrID cid.ID, token *handlers.BearerToken) { req := &models.ObjectUpload{ ContainerID: util.NewString(cnrID.EncodeToString()), FileName: util.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, token) checkGWErrorResponse(t, httpClient, request) } func checkGWErrorResponse(t *testing.T, httpClient *http.Client, request *http.Request) { resp := &models.ErrorResponse{} doRequest(t, httpClient, request, http.StatusBadRequest, resp) require.Equal(t, int64(0), resp.Code) require.Equal(t, models.ErrorTypeGW, *resp.Type) } 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{}, }}, }}, } bearer.Object = append(bearer.Object, getRestrictBearerRecords()...) httpClient := defaultHTTPClient() bearerTokens := makeAuthTokenRequest(ctx, t, []*models.Bearer{bearer}, httpClient, false) bearerToken := bearerTokens[0] content := "content of file" attrKey, attrValue := "User-Attribute", "user value" attributes := map[string]string{ object.AttributeFileName: "newFile.txt", attrKey: attrValue, } req := &models.ObjectUpload{ ContainerID: util.NewString(cnrID.EncodeToString()), FileName: util.NewString("newFile.txt"), Payload: base64.StdEncoding.EncodeToString([]byte(content)), Attributes: []*models.Attribute{{ Key: &attrKey, Value: &attrValue, }}, } body, err := json.Marshal(req) require.NoError(t, err) query := make(url.Values) query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) request, err := http.NewRequest(http.MethodPut, testHost+"/v1/objects?"+query.Encode(), bytes.NewReader(body)) require.NoError(t, err) prepareCommonHeaders(request.Header, bearerToken) addr := &models.Address{} doRequest(t, httpClient, request, http.StatusOK, addr) var CID cid.ID err = CID.DecodeString(*addr.ContainerID) require.NoError(t, err) var id oid.ID err = id.DecodeString(*addr.ObjectID) require.NoError(t, err) var objectAddress oid.Address objectAddress.SetContainer(CID) objectAddress.SetObject(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, ownerID *user.ID, cnrID cid.ID) { content := []byte("some content") attributes := map[string]string{ object.AttributeFileName: "get-obj-name", "user-attribute": "user value", } objID := createObject(ctx, t, p, ownerID, cnrID, attributes, content) bearer := &models.Bearer{ Object: []*models.Record{ { Operation: models.NewOperation(models.OperationHEAD), Action: models.NewAction(models.ActionALLOW), Filters: []*models.Filter{}, Targets: []*models.Target{{ Role: models.NewRole(models.RoleOTHERS), Keys: []string{}, }}, }, { Operation: models.NewOperation(models.OperationRANGE), Action: models.NewAction(models.ActionALLOW), Filters: []*models.Filter{}, Targets: []*models.Target{{ Role: models.NewRole(models.RoleOTHERS), Keys: []string{}, }}, }, }, } bearer.Object = append(bearer.Object, getRestrictBearerRecords()...) httpClient := defaultHTTPClient() bearerTokens := makeAuthTokenRequest(ctx, t, []*models.Bearer{bearer}, httpClient, false) bearerToken := bearerTokens[0] query := make(url.Values) query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) request, err := http.NewRequest(http.MethodGet, testHost+"/v1/objects/"+cnrID.EncodeToString()+"/"+objID.EncodeToString()+"?"+query.Encode(), nil) require.NoError(t, err) prepareCommonHeaders(request.Header, bearerToken) objInfo := &models.ObjectInfo{} doRequest(t, httpClient, request, http.StatusOK, objInfo) require.Equal(t, cnrID.EncodeToString(), *objInfo.ContainerID) require.Equal(t, objID.EncodeToString(), *objInfo.ObjectID) require.Equal(t, ownerID.EncodeToString(), *objInfo.OwnerID) require.Equal(t, len(attributes), len(objInfo.Attributes)) require.Equal(t, int64(len(content)), *objInfo.ObjectSize) contentData, err := base64.StdEncoding.DecodeString(objInfo.Payload) require.NoError(t, err) require.Equal(t, content, contentData) for _, attr := range objInfo.Attributes { require.Equal(t, attributes[*attr.Key], *attr.Value) } // check max-payload-size params query = make(url.Values) query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) query.Add("max-payload-size", "0") request, err = http.NewRequest(http.MethodGet, testHost+"/v1/objects/"+cnrID.EncodeToString()+"/"+objID.EncodeToString()+"?"+query.Encode(), nil) require.NoError(t, err) prepareCommonHeaders(request.Header, bearerToken) objInfo = &models.ObjectInfo{} doRequest(t, httpClient, request, http.StatusOK, objInfo) require.Empty(t, objInfo.Payload) require.Equal(t, int64(0), *objInfo.PayloadSize) // check range params rangeLength := 4 query = make(url.Values) query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) query.Add("range-offset", "0") query.Add("range-length", strconv.Itoa(rangeLength)) request, err = http.NewRequest(http.MethodGet, testHost+"/v1/objects/"+cnrID.EncodeToString()+"/"+objID.EncodeToString()+"?"+query.Encode(), nil) require.NoError(t, err) prepareCommonHeaders(request.Header, bearerToken) objInfo = &models.ObjectInfo{} doRequest(t, httpClient, request, http.StatusOK, objInfo) require.Equal(t, int64(rangeLength), *objInfo.PayloadSize) contentData, err = base64.StdEncoding.DecodeString(objInfo.Payload) require.NoError(t, err) require.Equal(t, content[:rangeLength], contentData) // check empty object objID2 := createObject(ctx, t, p, ownerID, cnrID, map[string]string{}, []byte{}) query2 := make(url.Values) query2.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) request2, err := http.NewRequest(http.MethodGet, testHost+"/v1/objects/"+cnrID.EncodeToString()+"/"+objID2.EncodeToString()+"?"+query2.Encode(), nil) require.NoError(t, err) prepareCommonHeaders(request2.Header, bearerToken) objInfo2 := &models.ObjectInfo{} doRequest(t, httpClient, request2, http.StatusOK, objInfo2) require.Equal(t, cnrID.EncodeToString(), *objInfo2.ContainerID) require.Equal(t, objID2.EncodeToString(), *objInfo2.ObjectID) require.Equal(t, ownerID.EncodeToString(), *objInfo2.OwnerID) require.Equal(t, 0, len(objInfo2.Attributes)) require.Equal(t, int64(0), *objInfo2.ObjectSize) } func restObjectGetUnauthenticated(ctx context.Context, t *testing.T, p *pool.Pool, ownerID *user.ID, cnrID cid.ID) { content := []byte("some content") attributes := map[string]string{ object.AttributeFileName: "get-obj-unauth-name", "user-attribute": "user value", } objID := createObject(ctx, t, p, ownerID, cnrID, attributes, content) httpClient := defaultHTTPClient() request, err := http.NewRequest(http.MethodGet, testHost+"/v1/objects/"+cnrID.EncodeToString()+"/"+objID.EncodeToString(), nil) require.NoError(t, err) request.Header.Add("Content-Type", "application/json") resp := &models.ErrorResponse{} doRequest(t, httpClient, request, http.StatusBadRequest, resp) require.Equal(t, int64(2048), resp.Code) require.Equal(t, models.ErrorTypeAPI, *resp.Type) // set empty eacl table to be able to do unauthenticated request allowByEACL(ctx, t, p, cnrID) request, err = http.NewRequest(http.MethodGet, testHost+"/v1/objects/"+cnrID.EncodeToString()+"/"+objID.EncodeToString(), nil) require.NoError(t, err) objInfo := &models.ObjectInfo{} doRequest(t, httpClient, request, http.StatusOK, objInfo) require.Equal(t, cnrID.EncodeToString(), *objInfo.ContainerID) require.Equal(t, objID.EncodeToString(), *objInfo.ObjectID) require.Equal(t, ownerID.EncodeToString(), *objInfo.OwnerID) require.Equal(t, len(attributes), len(objInfo.Attributes)) require.Equal(t, int64(len(content)), *objInfo.ObjectSize) contentData, err := base64.StdEncoding.DecodeString(objInfo.Payload) require.NoError(t, err) require.Equal(t, content, contentData) for _, attr := range objInfo.Attributes { require.Equal(t, attributes[*attr.Key], *attr.Value) } // set eacl the same as was before test started restrictByEACL(ctx, t, p, cnrID) } func restObjectGetFullBearer(ctx context.Context, t *testing.T, p *pool.Pool, ownerID *user.ID, cnrID cid.ID) { content := []byte("some content") attributes := map[string]string{ object.AttributeFileName: "get-obj-name", "user-attribute": "user value", } objID := createObject(ctx, t, p, ownerID, cnrID, attributes, content) bearers := &models.Bearer{ Object: []*models.Record{ { Operation: models.NewOperation(models.OperationHEAD), Action: models.NewAction(models.ActionALLOW), Filters: []*models.Filter{}, Targets: []*models.Target{{ Role: models.NewRole(models.RoleOTHERS), Keys: []string{}, }}, }, { Operation: models.NewOperation(models.OperationRANGE), Action: models.NewAction(models.ActionALLOW), Filters: []*models.Filter{}, Targets: []*models.Target{{ Role: models.NewRole(models.RoleOTHERS), Keys: []string{}, }}, }, }, } bearers.Object = append(bearers.Object, getRestrictBearerRecords()...) httpClient := defaultHTTPClient() bearerTokens := makeAuthTokenRequest(ctx, t, []*models.Bearer{bearers}, httpClient, true) bearerToken := bearerTokens[0] query := make(url.Values) query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) request, err := http.NewRequest(http.MethodGet, testHost+"/v1/auth/bearer?"+query.Encode(), nil) require.NoError(t, err) prepareCommonHeaders(request.Header, bearerToken) resp := &models.BinaryBearer{} doRequest(t, httpClient, request, http.StatusOK, resp) actualTokenRaw, err := base64.StdEncoding.DecodeString(*resp.Token) require.NoError(t, err) var actualToken bearer.Token err = actualToken.Unmarshal(actualTokenRaw) require.NoError(t, err) // check that is token for all users require.True(t, actualToken.AssertUser(user.ID{})) query.Add(fullBearerQuery, "true") request, err = http.NewRequest(http.MethodGet, testHost+"/v1/objects/"+cnrID.EncodeToString()+"/"+objID.EncodeToString()+"?"+query.Encode(), nil) require.NoError(t, err) request.Header.Add("Authorization", "Bearer "+*resp.Token) objInfo := &models.ObjectInfo{} doRequest(t, httpClient, request, http.StatusOK, objInfo) contentData, err := base64.StdEncoding.DecodeString(objInfo.Payload) require.NoError(t, err) require.Equal(t, content, contentData) } func restObjectDelete(ctx context.Context, t *testing.T, p *pool.Pool, owner *user.ID, cnrID cid.ID) { objID := createObject(ctx, t, p, owner, 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{}, }}, }}, } bearer.Object = append(bearer.Object, getRestrictBearerRecords()...) httpClient := defaultHTTPClient() bearerTokens := makeAuthTokenRequest(ctx, t, []*models.Bearer{bearer}, httpClient, false) bearerToken := bearerTokens[0] query := make(url.Values) query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) request, err := http.NewRequest(http.MethodDelete, testHost+"/v1/objects/"+cnrID.EncodeToString()+"/"+objID.EncodeToString()+"?"+query.Encode(), nil) require.NoError(t, err) prepareCommonHeaders(request.Header, bearerToken) resp := &models.SuccessResponse{} doRequest(t, httpClient, request, http.StatusOK, resp) require.True(t, *resp.Success) var addr oid.Address addr.SetContainer(cnrID) addr.SetObject(objID) var prm pool.PrmObjectHead prm.SetAddress(addr) _, err = p.HeadObject(ctx, prm) require.Error(t, err) } func restObjectsSearch(ctx context.Context, t *testing.T, p *pool.Pool, owner *user.ID, cnrID cid.ID) { userKey, userValue := "User-Attribute", "user-attribute-value" objectName := "object-name" filePath := "path/to/object/object-name" headers := map[string]string{ object.AttributeFileName: objectName, "FilePath": filePath, userKey: userValue, } objID := createObject(ctx, t, p, owner, cnrID, headers, []byte("some content")) headers[userKey] = "dummy" _ = createObject(ctx, t, p, owner, cnrID, headers, []byte("some content")) bearer := &models.Bearer{ Object: []*models.Record{ { Operation: models.NewOperation(models.OperationSEARCH), Action: models.NewAction(models.ActionALLOW), Filters: []*models.Filter{}, Targets: []*models.Target{{Role: models.NewRole(models.RoleOTHERS), Keys: []string{}}}, }, { Operation: models.NewOperation(models.OperationHEAD), Action: models.NewAction(models.ActionALLOW), Filters: []*models.Filter{}, Targets: []*models.Target{{Role: models.NewRole(models.RoleOTHERS), Keys: []string{}}}, }, { Operation: models.NewOperation(models.OperationGET), Action: models.NewAction(models.ActionALLOW), Filters: []*models.Filter{}, Targets: []*models.Target{{Role: models.NewRole(models.RoleOTHERS), Keys: []string{}}}, }, }, } bearer.Object = append(bearer.Object, getRestrictBearerRecords()...) httpClient := defaultHTTPClient() bearerTokens := makeAuthTokenRequest(ctx, t, []*models.Bearer{bearer}, httpClient, false) bearerToken := bearerTokens[0] search := &models.SearchFilters{ Filters: []*models.SearchFilter{ { Key: util.NewString(userKey), Match: models.NewSearchMatch(models.SearchMatchMatchStringEqual), Value: util.NewString(userValue), }, }, } body, err := json.Marshal(search) require.NoError(t, err) query := make(url.Values) query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) request, err := http.NewRequest(http.MethodPost, testHost+"/v1/objects/"+cnrID.EncodeToString()+"/search?"+query.Encode(), bytes.NewReader(body)) require.NoError(t, err) prepareCommonHeaders(request.Header, bearerToken) resp := &models.ObjectList{} doRequest(t, httpClient, request, http.StatusOK, resp) require.Equal(t, 1, int(*resp.Size)) require.Len(t, resp.Objects, 1) objBaseInfo := resp.Objects[0] require.Equal(t, cnrID.EncodeToString(), *objBaseInfo.Address.ContainerID) require.Equal(t, objID.EncodeToString(), *objBaseInfo.Address.ObjectID) require.Equal(t, objectName, objBaseInfo.Name) require.Equal(t, filePath, objBaseInfo.FilePath) } 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, owner user.ID, cnrID cid.ID) { httpClient := &http.Client{Timeout: 5 * time.Second} request, err := http.NewRequest(http.MethodGet, testHost+"/v1/containers/"+cnrID.EncodeToString(), nil) require.NoError(t, err) request = request.WithContext(ctx) cnrInfo := &models.ContainerInfo{} doRequest(t, httpClient, request, http.StatusOK, cnrInfo) require.Equal(t, cnrID.EncodeToString(), *cnrInfo.ContainerID) require.Equal(t, owner.EncodeToString(), *cnrInfo.OwnerID) require.Equal(t, containerName, *cnrInfo.ContainerName) require.NotEmpty(t, *cnrInfo.Version) } func restContainerDelete(ctx context.Context, t *testing.T, clientPool *pool.Pool, owner user.ID) { cnrID := createContainer(ctx, t, clientPool, owner, "for-delete") bearer := &models.Bearer{ Container: &models.Rule{ Verb: models.NewVerb(models.VerbDELETE), }, } httpClient := defaultHTTPClient() bearerTokens := makeAuthTokenRequest(ctx, t, []*models.Bearer{bearer}, httpClient, false) bearerToken := bearerTokens[0] query := make(url.Values) query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) request, err := http.NewRequest(http.MethodDelete, testHost+"/v1/containers/"+cnrID.EncodeToString()+"?"+query.Encode(), nil) require.NoError(t, err) request = request.WithContext(ctx) prepareCommonHeaders(request.Header, bearerToken) resp := &models.SuccessResponse{} doRequest(t, httpClient, request, http.StatusOK, resp) require.True(t, *resp.Success) 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, owner user.ID) { cnrID := createContainer(ctx, t, clientPool, owner, "for-eacl-put") httpClient := &http.Client{Timeout: 60 * time.Second} bearer := &models.Bearer{ Container: &models.Rule{ Verb: models.NewVerb(models.VerbSETEACL), }, } bearerTokens := makeAuthTokenRequest(ctx, t, []*models.Bearer{bearer}, httpClient, false) bearerToken := bearerTokens[0] req := models.Eacl{ Records: []*models.Record{{ Action: models.NewAction(models.ActionDENY), Filters: []*models.Filter{}, Operation: models.NewOperation(models.OperationDELETE), Targets: []*models.Target{{ Keys: []string{"031a6c6fbbdf02ca351745fa86b9ba5a9452d785ac4f7fc2b7548ca2a46c4fcf4a"}, Role: models.NewRole(models.RoleOTHERS), }}, }}, } invalidBody, err := json.Marshal(&req) require.NoError(t, err) req.Records[0].Targets[0].Role = models.NewRole(models.RoleKEYS) body, err := json.Marshal(&req) require.NoError(t, err) query := make(url.Values) query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) doSetEACLRequest(ctx, t, httpClient, cnrID, query, bearerToken, invalidBody, http.StatusBadRequest, nil) resp := &models.SuccessResponse{} doSetEACLRequest(ctx, t, httpClient, cnrID, query, bearerToken, body, http.StatusOK, resp) require.True(t, *resp.Success) var prm pool.PrmContainerEACL prm.SetContainerID(cnrID) table, err := clientPool.GetEACL(ctx, prm) require.NoError(t, err) expectedTable, err := util.ToNativeTable(req.Records) require.NoError(t, err) expectedTable.SetCID(cnrID) require.True(t, eacl.EqualTables(*expectedTable, table)) } func doSetEACLRequest(ctx context.Context, t *testing.T, httpClient *http.Client, cnrID cid.ID, query url.Values, bearerToken *handlers.BearerToken, body []byte, status int, model interface{}) { request, err := http.NewRequest(http.MethodPut, testHost+"/v1/containers/"+cnrID.EncodeToString()+"/eacl?"+query.Encode(), bytes.NewReader(body)) require.NoError(t, err) request = request.WithContext(ctx) prepareCommonHeaders(request.Header, bearerToken) doRequest(t, httpClient, request, status, model) } 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.EncodeToString()+"/eacl", nil) require.NoError(t, err) request = request.WithContext(ctx) responseTable := &models.Eacl{} doRequest(t, httpClient, request, http.StatusOK, responseTable) require.Equal(t, cnrID.EncodeToString(), responseTable.ContainerID) actualTable, err := util.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, owner user.ID, cnrID cid.ID) { var prm pool.PrmContainerList prm.SetOwnerID(owner) ids, err := p.ListContainers(ctx, prm) require.NoError(t, err) httpClient := defaultHTTPClient() query := make(url.Values) query.Add("ownerId", owner.EncodeToString()) 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)) require.Truef(t, containsContainer(list.Containers, cnrID.EncodeToString(), containerName), "list doesn't contain cnr '%s' with name '%s'", cnrID.EncodeToString(), containerName) } func containsContainer(containers []*models.ContainerInfo, cnrID, cnrName string) bool { for _, cnrInfo := range containers { if *cnrInfo.ContainerID == cnrID { for _, attr := range cnrInfo.Attributes { if *attr.Key == "Name" && *attr.Value == cnrName { return true } } fmt.Println("container found but name doesn't match") return false } } return false } func makeAuthTokenRequest(ctx context.Context, t *testing.T, bearers []*models.Bearer, httpClient *http.Client, forAllUsers bool) []*handlers.BearerToken { key, err := keys.NewPrivateKeyFromHex(devenvPrivateKey) require.NoError(t, err) var ownerID user.ID user.IDFromKey(&ownerID, key.PrivateKey.PublicKey) data, err := json.Marshal(bearers) 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(XBearerOwnerID, ownerID.String()) request.Header.Add(XBearerForAllUsers, strconv.FormatBool(forAllUsers)) 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) var stokenResp []*models.TokenResponse err = json.Unmarshal(rr, &stokenResp) require.NoError(t, err) fmt.Println("resp tokens:") respTokens := make([]*handlers.BearerToken, len(stokenResp)) for i, tok := range stokenResp { isObject, err := handlers.IsObjectToken(bearers[i]) require.NoError(t, err) require.Equal(t, bearers[i].Name, tok.Name) if isObject { require.Equal(t, models.TokenTypeObject, *tok.Type) } else { require.Equal(t, models.TokenTypeContainer, *tok.Type) } binaryData, err := base64.StdEncoding.DecodeString(*tok.Token) require.NoError(t, err) var bt *handlers.BearerToken if useWalletConnect { bt = signTokenWalletConnect(t, key, binaryData) } else { bt = signToken(t, key, binaryData) } respTokens[i] = bt fmt.Printf("%+v\n", bt) } return respTokens } func signToken(t *testing.T, key *keys.PrivateKey, data []byte) *handlers.BearerToken { signer := neofsecdsa.Signer(key.PrivateKey) sign, err := signer.Sign(data) require.NoError(t, err) return &handlers.BearerToken{ Token: base64.StdEncoding.EncodeToString(data), Signature: hex.EncodeToString(sign), Key: hex.EncodeToString(key.PublicKey().Bytes()), } } func signTokenWalletConnect(t *testing.T, key *keys.PrivateKey, data []byte) *handlers.BearerToken { signer := neofsecdsa.SignerWalletConnect(key.PrivateKey) signature, err := signer.Sign(data) require.NoError(t, err) return &handlers.BearerToken{ Token: base64.StdEncoding.EncodeToString(data), Signature: hex.EncodeToString(signature), Key: hex.EncodeToString(key.PublicKey().Bytes()), } } func restContainerPutInvalid(ctx context.Context, t *testing.T) { bearer := &models.Bearer{ Container: &models.Rule{ Verb: models.NewVerb(models.VerbPUT), }, } httpClient := &http.Client{Timeout: 30 * time.Second} bearerTokens := makeAuthTokenRequest(ctx, t, []*models.Bearer{bearer}, httpClient, false) bearerToken := bearerTokens[0] reqURL, err := url.Parse(testHost + "/v1/containers") require.NoError(t, err) query := reqURL.Query() query.Add("name-scope-global", "true") query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) reqURL.RawQuery = query.Encode() body, err := json.Marshal(&models.ContainerPutInfo{ContainerName: "nameWithCapitalLetters"}) require.NoError(t, err) request, err := http.NewRequest(http.MethodPut, reqURL.String(), bytes.NewReader(body)) require.NoError(t, err) prepareCommonHeaders(request.Header, bearerToken) resp := &models.ErrorResponse{} doRequest(t, httpClient, request, http.StatusBadRequest, resp) require.Equal(t, int64(0), resp.Code) require.Equal(t, models.ErrorTypeGW, *resp.Type) } 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} bearerTokens := makeAuthTokenRequest(ctx, t, []*models.Bearer{bearer}, httpClient, false) bearerToken := bearerTokens[0] attrKey, attrValue := "User-Attribute", "user value" userAttributes := map[string]string{ attrKey: attrValue, } // try to create container without name but with name-scope-global body, err := json.Marshal(&models.ContainerPutInfo{}) require.NoError(t, err) reqURL, err := url.Parse(testHost + "/v1/containers") require.NoError(t, err) query := reqURL.Query() query.Add("name-scope-global", "true") query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) reqURL.RawQuery = query.Encode() request, err := http.NewRequest(http.MethodPut, reqURL.String(), bytes.NewReader(body)) require.NoError(t, err) prepareCommonHeaders(request.Header, bearerToken) doRequest(t, httpClient, request, http.StatusBadRequest, nil) // create container with name in local scope containerPutInfo := &models.ContainerPutInfo{ Attributes: []*models.Attribute{{ Key: util.NewString(attrKey), Value: util.NewString(attrValue), }}, } body, err = json.Marshal(containerPutInfo) require.NoError(t, err) reqURL, err = url.Parse(testHost + "/v1/containers") require.NoError(t, err) query = reqURL.Query() query.Add(walletConnectQuery, strconv.FormatBool(useWalletConnect)) reqURL.RawQuery = query.Encode() request, err = http.NewRequest(http.MethodPut, reqURL.String(), bytes.NewReader(body)) require.NoError(t, err) prepareCommonHeaders(request.Header, bearerToken) addr := &operations.PutContainerOKBody{} doRequest(t, httpClient, request, http.StatusOK, addr) var CID cid.ID err = CID.DecodeString(*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) cnr.IterateAttributes(func(key, val string) { cnrAttr[key] = val }) 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, owner user.ID, name string) cid.ID { var policy netmap.PlacementPolicy err := policy.DecodeString("REP 1") require.NoError(t, err) var cnr container.Container cnr.Init() cnr.SetOwner(owner) cnr.SetPlacementPolicy(policy) cnr.SetBasicACL(acl.PublicRWExtended) container.SetName(&cnr, name) container.SetCreationTime(&cnr, time.Now()) err = pool.SyncContainerWithNetwork(ctx, &cnr, clientPool) require.NoError(t, err) 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, ownerID *user.ID, 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(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 } func allowByEACL(ctx context.Context, t *testing.T, clientPool *pool.Pool, cnrID cid.ID) *eacl.Table { table := eacl.NewTable() table.SetCID(cnrID) 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 }