Correlations: Add DeleteCorrelation HTTP API (#51801)

* Correlations: add DeleteCorrelation HTTP API

* fix error message copy

* add readonly check

* add source_uid in delete condition

* make path singular

* Revert "make path singular"

This reverts commit d15be89578e202e5cb64a3e964ee09521b72d87c.

* add tests

* fix lint errors

* fix lint errors

* change casing

* update spec

* Remove transaction

* change casing in param name in docs
This commit is contained in:
Giordano Ricci
2022-07-27 09:07:58 +01:00
committed by GitHub
parent 52989c2144
commit 9a06b00e92
11 changed files with 505 additions and 14 deletions
+46 -9
View File
@@ -8,12 +8,18 @@ import (
"testing"
"github.com/grafana/grafana/pkg/server"
"github.com/grafana/grafana/pkg/services/correlations"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/stretchr/testify/require"
)
type errorResponseBody struct {
Message string `json:"message"`
Error string `json:"error"`
}
type TestContext struct {
env server.TestEnv
t *testing.T
@@ -46,18 +52,10 @@ type PostParams struct {
func (c TestContext) Post(params PostParams) *http.Response {
c.t.Helper()
buf := bytes.NewReader([]byte(params.body))
baseUrl := fmt.Sprintf("http://%s", c.env.Server.HTTPServer.Listener.Addr())
if params.user.username != "" && params.user.password != "" {
baseUrl = fmt.Sprintf("http://%s:%s@%s", params.user.username, params.user.password, c.env.Server.HTTPServer.Listener.Addr())
}
// nolint:gosec
resp, err := http.Post(
fmt.Sprintf(
"%s%s",
baseUrl,
params.url,
),
c.getURL(params.url, params.user),
"application/json",
buf,
)
@@ -66,6 +64,37 @@ func (c TestContext) Post(params PostParams) *http.Response {
return resp
}
type DeleteParams struct {
url string
user User
}
func (c TestContext) Delete(params DeleteParams) *http.Response {
c.t.Helper()
req, err := http.NewRequest("DELETE", c.getURL(params.url, params.user), nil)
require.NoError(c.t, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(c.t, err)
return resp
}
func (c TestContext) getURL(url string, user User) string {
c.t.Helper()
baseUrl := fmt.Sprintf("http://%s", c.env.Server.HTTPServer.Listener.Addr())
if user.username != "" && user.password != "" {
baseUrl = fmt.Sprintf("http://%s:%s@%s", user.username, user.password, c.env.Server.HTTPServer.Listener.Addr())
}
return fmt.Sprintf(
"%s%s",
baseUrl,
url,
)
}
func (c TestContext) createUser(cmd user.CreateUserCommand) {
c.t.Helper()
@@ -82,3 +111,11 @@ func (c TestContext) createDs(cmd *datasources.AddDataSourceCommand) {
err := c.env.SQLStore.AddDataSource(context.Background(), cmd)
require.NoError(c.t, err)
}
func (c TestContext) createCorrelation(cmd correlations.CreateCorrelationCommand) correlations.Correlation {
c.t.Helper()
correlation, err := c.env.Server.HTTPServer.CorrelationsService.CreateCorrelation(context.Background(), cmd)
require.NoError(c.t, err)
return correlation
}
@@ -14,11 +14,6 @@ import (
"github.com/stretchr/testify/require"
)
type errorResponseBody struct {
Message string `json:"message"`
Error string `json:"error"`
}
func TestIntegrationCreateCorrelation(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
@@ -0,0 +1,222 @@
package correlations
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"testing"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/correlations"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/user"
"github.com/stretchr/testify/require"
)
func TestIntegrationDeleteCorrelation(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
ctx := NewTestEnv(t)
adminUser := User{
username: "admin",
password: "admin",
}
editorUser := User{
username: "editor",
password: "editor",
}
ctx.createUser(user.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_EDITOR),
Password: editorUser.password,
Login: editorUser.username,
})
ctx.createUser(user.CreateUserCommand{
DefaultOrgRole: string(models.ROLE_ADMIN),
Password: adminUser.password,
Login: adminUser.username,
})
createDsCommand := &datasources.AddDataSourceCommand{
Name: "read-only",
Type: "loki",
ReadOnly: true,
OrgId: 1,
}
ctx.createDs(createDsCommand)
readOnlyDS := createDsCommand.Result.Uid
createDsCommand = &datasources.AddDataSourceCommand{
Name: "writable",
Type: "loki",
OrgId: 1,
}
ctx.createDs(createDsCommand)
writableDs := createDsCommand.Result.Uid
writableDsOrgId := createDsCommand.Result.OrgId
t.Run("Unauthenticated users shouldn't be able to delete correlations", func(t *testing.T) {
res := ctx.Delete(DeleteParams{
url: fmt.Sprintf("/api/datasources/uid/%s/correlations/%s", "some-ds-uid", "some-correlation-uid"),
})
require.Equal(t, http.StatusUnauthorized, res.StatusCode)
responseBody, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)
var response errorResponseBody
err = json.Unmarshal(responseBody, &response)
require.NoError(t, err)
require.Equal(t, "Unauthorized", response.Message)
require.NoError(t, res.Body.Close())
})
t.Run("non org admin shouldn't be able to delete correlations", func(t *testing.T) {
res := ctx.Delete(DeleteParams{
url: fmt.Sprintf("/api/datasources/uid/%s/correlations/%s", "some-ds-uid", "some-correlation-uid"),
user: editorUser,
})
require.Equal(t, http.StatusForbidden, res.StatusCode)
responseBody, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)
var response errorResponseBody
err = json.Unmarshal(responseBody, &response)
require.NoError(t, err)
require.Contains(t, response.Message, "Permissions needed: datasources:write")
require.NoError(t, res.Body.Close())
})
t.Run("inexistent source data source should result in a 404", func(t *testing.T) {
res := ctx.Delete(DeleteParams{
url: fmt.Sprintf("/api/datasources/uid/%s/correlations/%s", "nonexistent-ds-uid", "some-correlation-uid"),
user: adminUser,
})
require.Equal(t, http.StatusNotFound, res.StatusCode)
responseBody, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)
var response errorResponseBody
err = json.Unmarshal(responseBody, &response)
require.NoError(t, err)
require.Equal(t, "Data source not found", response.Message)
require.Equal(t, correlations.ErrSourceDataSourceDoesNotExists.Error(), response.Error)
require.NoError(t, res.Body.Close())
})
t.Run("inexistent correlation should result in a 404", func(t *testing.T) {
res := ctx.Delete(DeleteParams{
url: fmt.Sprintf("/api/datasources/uid/%s/correlations/%s", writableDs, "nonexistent-correlation-uid"),
user: adminUser,
})
require.Equal(t, http.StatusNotFound, res.StatusCode)
responseBody, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)
var response errorResponseBody
err = json.Unmarshal(responseBody, &response)
require.NoError(t, err)
require.Equal(t, "Correlation not found", response.Message)
require.Equal(t, correlations.ErrCorrelationNotFound.Error(), response.Error)
require.NoError(t, res.Body.Close())
})
t.Run("deleting a correlation originating from a read-only data source should result in a 403", func(t *testing.T) {
res := ctx.Delete(DeleteParams{
url: fmt.Sprintf("/api/datasources/uid/%s/correlations/%s", readOnlyDS, "nonexistent-correlation-uid"),
user: adminUser,
})
require.Equal(t, http.StatusForbidden, res.StatusCode)
responseBody, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)
var response errorResponseBody
err = json.Unmarshal(responseBody, &response)
require.NoError(t, err)
require.Equal(t, "Data source is read only", response.Message)
require.Equal(t, correlations.ErrSourceDataSourceReadOnly.Error(), response.Error)
require.NoError(t, res.Body.Close())
})
t.Run("deleting a correlation pointing to a read-only data source should work", func(t *testing.T) {
correlation := ctx.createCorrelation(correlations.CreateCorrelationCommand{
SourceUID: writableDs,
TargetUID: writableDs,
OrgId: writableDsOrgId,
})
res := ctx.Delete(DeleteParams{
url: fmt.Sprintf("/api/datasources/uid/%s/correlations/%s", correlation.SourceUID, correlation.UID),
user: adminUser,
})
require.Equal(t, http.StatusOK, res.StatusCode)
responseBody, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)
var response correlations.CreateCorrelationResponse
err = json.Unmarshal(responseBody, &response)
require.NoError(t, err)
require.Equal(t, "Correlation deleted", response.Message)
require.NoError(t, res.Body.Close())
// trying to delete the same correlation a second time should result in a 404
res = ctx.Delete(DeleteParams{
url: fmt.Sprintf("/api/datasources/uid/%s/correlations/%s", correlation.SourceUID, correlation.UID),
user: adminUser,
})
require.NoError(t, res.Body.Close())
require.Equal(t, http.StatusNotFound, res.StatusCode)
})
t.Run("should correctly delete a correlation", func(t *testing.T) {
correlation := ctx.createCorrelation(correlations.CreateCorrelationCommand{
SourceUID: writableDs,
TargetUID: readOnlyDS,
OrgId: writableDsOrgId,
})
res := ctx.Delete(DeleteParams{
url: fmt.Sprintf("/api/datasources/uid/%s/correlations/%s", correlation.SourceUID, correlation.UID),
user: adminUser,
})
require.Equal(t, http.StatusOK, res.StatusCode)
responseBody, err := ioutil.ReadAll(res.Body)
require.NoError(t, err)
var response correlations.CreateCorrelationResponse
err = json.Unmarshal(responseBody, &response)
require.NoError(t, err)
require.Equal(t, "Correlation deleted", response.Message)
require.NoError(t, res.Body.Close())
// trying to delete the same correlation a second time should result in a 404
res = ctx.Delete(DeleteParams{
url: fmt.Sprintf("/api/datasources/uid/%s/correlations/%s", correlation.SourceUID, correlation.UID),
user: adminUser,
})
require.NoError(t, res.Body.Close())
require.Equal(t, http.StatusNotFound, res.StatusCode)
})
}