PublicDashboards: Validate access token (#57298)

Adding validation for access token
This commit is contained in:
lean.dev 2022-10-20 16:43:33 -03:00 committed by GitHub
parent 5ee4744d62
commit 552d9d70eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 118 additions and 59 deletions

View File

@ -17,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/publicdashboards"
"github.com/grafana/grafana/pkg/services/publicdashboards/internal/tokens"
. "github.com/grafana/grafana/pkg/services/publicdashboards/models"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
@ -52,7 +53,7 @@ func ProvideApi(
return api
}
// Registers Endpoints on Grafana Router
// RegisterAPIEndpoints Registers Endpoints on Grafana Router
func (api *Api) RegisterAPIEndpoints() {
auth := accesscontrol.Middleware(api.AccessControl)
@ -79,11 +80,15 @@ func (api *Api) RegisterAPIEndpoints() {
routing.Wrap(api.SavePublicDashboardConfig))
}
// Gets public dashboard
// GetPublicDashboard Gets public dashboard
// GET /api/public/dashboards/:accessToken
func (api *Api) GetPublicDashboard(c *models.ReqContext) response.Response {
accessToken := web.Params(c.Req)[":accessToken"]
if !tokens.IsValidAccessToken(accessToken) {
return response.Error(http.StatusBadRequest, "Invalid Access Token", nil)
}
pubdash, dash, err := api.PublicDashboardService.GetPublicDashboard(
c.Req.Context(),
accessToken,
@ -115,7 +120,7 @@ func (api *Api) GetPublicDashboard(c *models.ReqContext) response.Response {
return response.JSON(http.StatusOK, dto)
}
// Gets list of public dashboards for an org
// ListPublicDashboards Gets list of public dashboards for an org
// GET /api/dashboards/public
func (api *Api) ListPublicDashboards(c *models.ReqContext) response.Response {
resp, err := api.PublicDashboardService.ListPublicDashboards(c.Req.Context(), c.SignedInUser, c.OrgID)
@ -125,7 +130,7 @@ func (api *Api) ListPublicDashboards(c *models.ReqContext) response.Response {
return response.JSON(http.StatusOK, resp)
}
// Gets public dashboard configuration for dashboard
// GetPublicDashboardConfig Gets public dashboard configuration for dashboard
// GET /api/dashboards/uid/:uid/public-config
func (api *Api) GetPublicDashboardConfig(c *models.ReqContext) response.Response {
pdc, err := api.PublicDashboardService.GetPublicDashboardConfig(c.Req.Context(), c.OrgID, web.Params(c.Req)[":uid"])
@ -135,7 +140,7 @@ func (api *Api) GetPublicDashboardConfig(c *models.ReqContext) response.Response
return response.JSON(http.StatusOK, pdc)
}
// Sets public dashboard configuration for dashboard
// SavePublicDashboardConfig Sets public dashboard configuration for dashboard
// POST /api/dashboards/uid/:uid/public-config
func (api *Api) SavePublicDashboardConfig(c *models.ReqContext) response.Response {
// exit if we don't have a valid dashboardUid
@ -170,6 +175,11 @@ func (api *Api) SavePublicDashboardConfig(c *models.ReqContext) response.Respons
// QueryPublicDashboard returns all results for a given panel on a public dashboard
// POST /api/public/dashboard/:accessToken/panels/:panelId/query
func (api *Api) QueryPublicDashboard(c *models.ReqContext) response.Response {
accessToken := web.Params(c.Req)[":accessToken"]
if !tokens.IsValidAccessToken(accessToken) {
return response.Error(http.StatusBadRequest, "Invalid Access Token", nil)
}
panelId, err := strconv.ParseInt(web.Params(c.Req)[":panelId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "QueryPublicDashboard: invalid panel ID", err)
@ -180,7 +190,7 @@ func (api *Api) QueryPublicDashboard(c *models.ReqContext) response.Response {
return response.Error(http.StatusBadRequest, "QueryPublicDashboard: bad request data", err)
}
resp, err := api.PublicDashboardService.GetQueryDataResponse(c.Req.Context(), c.SkipCache, reqDTO, panelId, web.Params(c.Req)[":accessToken"])
resp, err := api.PublicDashboardService.GetQueryDataResponse(c.Req.Context(), c.SkipCache, reqDTO, panelId, accessToken)
if err != nil {
return api.handleError(c.Req.Context(), http.StatusInternalServerError, "QueryPublicDashboard: error running public dashboard panel queries", err)
}
@ -188,13 +198,20 @@ func (api *Api) QueryPublicDashboard(c *models.ReqContext) response.Response {
return toJsonStreamingResponse(api.Features, resp)
}
// GetAnnotations returns annotations for a public dashboard
// GET /api/public/dashboards/:accessToken/annotations
func (api *Api) GetAnnotations(c *models.ReqContext) response.Response {
accessToken := web.Params(c.Req)[":accessToken"]
if !tokens.IsValidAccessToken(accessToken) {
return response.Error(http.StatusBadRequest, "Invalid Access Token", nil)
}
reqDTO := AnnotationsQueryDTO{
From: c.QueryInt64("from"),
To: c.QueryInt64("to"),
}
annotations, err := api.PublicDashboardService.GetAnnotations(c.Req.Context(), reqDTO, web.Params(c.Req)[":accessToken"])
annotations, err := api.PublicDashboardService.GetAnnotations(c.Req.Context(), reqDTO, accessToken)
if err != nil {
return api.handleError(c.Req.Context(), http.StatusInternalServerError, "error getting public dashboard annotations", err)

View File

@ -10,7 +10,6 @@ import (
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/google/uuid"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/stretchr/testify/assert"
@ -57,24 +56,40 @@ func TestAPIGetAnnotations(t *testing.T) {
ExpectedHttpResponse int
Annotations []AnnotationEvent
ServiceError error
AccessToken string
From string
To string
ExpectedServiceCalled bool
}{
{
Name: "will return success when there is no error and to and from are provided",
ExpectedHttpResponse: http.StatusOK,
Annotations: []AnnotationEvent{{Id: 1}},
ServiceError: nil,
AccessToken: validAccessToken,
From: "123",
To: "123",
ExpectedServiceCalled: true,
},
{
Name: "will return 500 when service returns an error",
ExpectedHttpResponse: http.StatusInternalServerError,
Annotations: nil,
ServiceError: errors.New("an error happened"),
AccessToken: validAccessToken,
From: "123",
To: "123",
ExpectedServiceCalled: true,
},
{
Name: "will return 400 when has an incorrect Access Token",
ExpectedHttpResponse: http.StatusBadRequest,
Annotations: nil,
ServiceError: errors.New("an error happened"),
AccessToken: "TooShortAccessToken",
From: "123",
To: "123",
ExpectedServiceCalled: false,
},
}
for _, test := range testCases {
@ -82,11 +97,15 @@ func TestAPIGetAnnotations(t *testing.T) {
cfg := setting.NewCfg()
cfg.RBACEnabled = false
service := publicdashboards.NewFakePublicDashboardService(t)
if test.ExpectedServiceCalled {
service.On("GetAnnotations", mock.Anything, mock.Anything, mock.AnythingOfType("string")).
Return(test.Annotations, test.ServiceError).Once()
}
testServer := setupTestServer(t, cfg, featuremgmt.WithFeatures(featuremgmt.FlagPublicDashboards), service, nil, anonymousUser)
path := fmt.Sprintf("/api/public/dashboards/abc123/annotations?from=%s&to=%s", test.From, test.To)
path := fmt.Sprintf("/api/public/dashboards/%s/annotations?from=%s&to=%s", test.AccessToken, test.From, test.To)
response := callAPI(testServer, http.MethodGet, path, nil, t)
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
@ -221,9 +240,6 @@ func TestAPIListPublicDashboard(t *testing.T) {
func TestAPIGetPublicDashboard(t *testing.T) {
DashboardUid := "dashboard-abcd1234"
token, err := uuid.NewRandom()
require.NoError(t, err)
accessToken := fmt.Sprintf("%x", token)
testCases := []struct {
Name string
@ -231,10 +247,11 @@ func TestAPIGetPublicDashboard(t *testing.T) {
ExpectedHttpResponse int
DashboardResult *models.Dashboard
Err error
FixedErrorResponse string
}{
{
Name: "It gets a public dashboard",
AccessToken: accessToken,
AccessToken: validAccessToken,
ExpectedHttpResponse: http.StatusOK,
DashboardResult: &models.Dashboard{
Data: simplejson.NewFromAny(map[string]interface{}{
@ -242,13 +259,23 @@ func TestAPIGetPublicDashboard(t *testing.T) {
}),
},
Err: nil,
FixedErrorResponse: "",
},
{
Name: "It should return 404 if no public dashboard",
AccessToken: accessToken,
AccessToken: validAccessToken,
ExpectedHttpResponse: http.StatusNotFound,
DashboardResult: nil,
Err: ErrPublicDashboardNotFound,
FixedErrorResponse: "",
},
{
Name: "It should return 400 if it is an invalid access token",
AccessToken: "SomeInvalidAccessToken",
ExpectedHttpResponse: http.StatusBadRequest,
DashboardResult: nil,
Err: nil,
FixedErrorResponse: "{\"message\":\"Invalid Access Token\"}",
},
}
@ -278,7 +305,7 @@ func TestAPIGetPublicDashboard(t *testing.T) {
assert.Equal(t, test.ExpectedHttpResponse, response.Code)
if test.Err == nil {
if test.Err == nil && test.FixedErrorResponse == "" {
var dashResp dtos.DashboardFullWithMeta
err := json.Unmarshal(response.Body.Bytes(), &dashResp)
require.NoError(t, err)
@ -287,6 +314,9 @@ func TestAPIGetPublicDashboard(t *testing.T) {
assert.Equal(t, false, dashResp.Meta.CanEdit)
assert.Equal(t, false, dashResp.Meta.CanDelete)
assert.Equal(t, false, dashResp.Meta.CanSave)
} else if test.FixedErrorResponse != "" {
require.Equal(t, test.ExpectedHttpResponse, response.Code)
require.JSONEq(t, "{\"message\":\"Invalid Access Token\"}", response.Body.String())
} else {
var errResp JsonErrResponse
err := json.Unmarshal(response.Body.Bytes(), &errResp)
@ -585,29 +615,37 @@ func TestAPIQueryPublicDashboard(t *testing.T) {
t.Run("Status code is 400 when the panel ID is invalid", func(t *testing.T) {
server, _ := setup(true)
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/notanumber/query", strings.NewReader("{}"), t)
path := fmt.Sprintf("/api/public/dashboards/%s/panels/notanumber/query", validAccessToken)
resp := callAPI(server, http.MethodPost, path, strings.NewReader("{}"), t)
require.Equal(t, http.StatusBadRequest, resp.Code)
})
t.Run("Status code is 400 when the access token is invalid", func(t *testing.T) {
server, _ := setup(true)
resp := callAPI(server, http.MethodPost, getValidQueryPath("SomeInvalidAccessToken"), strings.NewReader("{}"), t)
require.Equal(t, http.StatusBadRequest, resp.Code)
require.JSONEq(t, "{\"message\":\"Invalid Access Token\"}", resp.Body.String())
})
t.Run("Status code is 400 when the intervalMS is lesser than 0", func(t *testing.T) {
server, fakeDashboardService := setup(true)
fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), "abc123").Return(&backend.QueryDataResponse{}, ErrPublicDashboardBadRequest)
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader(`{"intervalMs":-100,"maxDataPoints":1000}`), t)
fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), validAccessToken).Return(&backend.QueryDataResponse{}, ErrPublicDashboardBadRequest)
resp := callAPI(server, http.MethodPost, getValidQueryPath(validAccessToken), strings.NewReader(`{"intervalMs":-100,"maxDataPoints":1000}`), t)
require.Equal(t, http.StatusBadRequest, resp.Code)
})
t.Run("Status code is 400 when the maxDataPoints is lesser than 0", func(t *testing.T) {
server, fakeDashboardService := setup(true)
fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), "abc123").Return(&backend.QueryDataResponse{}, ErrPublicDashboardBadRequest)
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader(`{"intervalMs":100,"maxDataPoints":-1000}`), t)
fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), validAccessToken).Return(&backend.QueryDataResponse{}, ErrPublicDashboardBadRequest)
resp := callAPI(server, http.MethodPost, getValidQueryPath(validAccessToken), strings.NewReader(`{"intervalMs":100,"maxDataPoints":-1000}`), t)
require.Equal(t, http.StatusBadRequest, resp.Code)
})
t.Run("Returns query data when feature toggle is enabled", func(t *testing.T) {
server, fakeDashboardService := setup(true)
fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), "abc123").Return(mockedResponse, nil)
fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), validAccessToken).Return(mockedResponse, nil)
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t)
resp := callAPI(server, http.MethodPost, getValidQueryPath(validAccessToken), strings.NewReader("{}"), t)
require.JSONEq(
t,
@ -619,13 +657,17 @@ func TestAPIQueryPublicDashboard(t *testing.T) {
t.Run("Status code is 500 when the query fails", func(t *testing.T) {
server, fakeDashboardService := setup(true)
fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), "abc123").Return(&backend.QueryDataResponse{}, fmt.Errorf("error"))
fakeDashboardService.On("GetQueryDataResponse", mock.Anything, true, mock.Anything, int64(2), validAccessToken).Return(&backend.QueryDataResponse{}, fmt.Errorf("error"))
resp := callAPI(server, http.MethodPost, "/api/public/dashboards/abc123/panels/2/query", strings.NewReader("{}"), t)
resp := callAPI(server, http.MethodPost, getValidQueryPath(validAccessToken), strings.NewReader("{}"), t)
require.Equal(t, http.StatusInternalServerError, resp.Code)
})
}
func getValidQueryPath(accessToken string) string {
return fmt.Sprintf("/api/public/dashboards/%s/panels/2/query", accessToken)
}
func TestIntegrationUnauthenticatedUserCanGetPubdashPanelQueryData(t *testing.T) {
db := db.InitTestDB(t)

View File

@ -10,7 +10,7 @@ import (
"github.com/grafana/grafana/pkg/web"
)
// Adds orgId to context based on org of public dashboard
// SetPublicDashboardOrgIdOnContext Adds orgId to context based on org of public dashboard
func SetPublicDashboardOrgIdOnContext(publicDashboardService publicdashboards.Service) func(c *models.ReqContext) {
return func(c *models.ReqContext) {
accessToken, ok := web.Params(c.Req)[":accessToken"]
@ -28,14 +28,15 @@ func SetPublicDashboardOrgIdOnContext(publicDashboardService publicdashboards.Se
}
}
// Adds public dashboard flag on context
// SetPublicDashboardFlag Adds public dashboard flag on context
func SetPublicDashboardFlag(c *models.ReqContext) {
c.IsPublicDashboardView = true
}
// Middleware to enforce that a public dashboards exists before continuing to
// handler
func RequiresValidAccessToken(publicDashboardService publicdashboards.Service) func(c *models.ReqContext) {
// RequiresExistingAccessToken Middleware to enforce that a public dashboards exists before continuing to handler. This
// method will query the database to ensure that it exists.
// Use when we want to enforce a public dashboard is valid on an endpoint we do not maintain
func RequiresExistingAccessToken(publicDashboardService publicdashboards.Service) func(c *models.ReqContext) {
return func(c *models.ReqContext) {
accessToken, ok := web.Params(c.Req)[":accessToken"]

View File

@ -20,7 +20,7 @@ import (
var validAccessToken, _ = tokens.GenerateAccessToken()
func TestRequiresValidAccessToken(t *testing.T) {
func TestRequiresExistingAccessToken(t *testing.T) {
tests := []struct {
Name string
Path string
@ -76,7 +76,7 @@ func TestRequiresValidAccessToken(t *testing.T) {
publicdashboardService := &publicdashboards.FakePublicDashboardService{}
publicdashboardService.On("AccessTokenExists", mock.Anything, mock.Anything).Return(tt.AccessTokenExists, tt.AccessTokenExistsErr)
params := map[string]string{":accessToken": tt.AccessToken}
mw := RequiresValidAccessToken(publicdashboardService)
mw := RequiresExistingAccessToken(publicdashboardService)
_, resp := runMw(t, nil, "GET", tt.Path, params, mw)
require.Equal(t, tt.ExpectedResponseCode, resp.Code)
})

View File

@ -6,17 +6,16 @@ import (
"github.com/google/uuid"
)
// generates a uuid formatted without dashes to use as access token
// GenerateAccessToken generates an uuid formatted without dashes to use as access token
func GenerateAccessToken() (string, error) {
token, err := uuid.NewRandom()
if err != nil {
return "", err
}
return fmt.Sprintf("%x", token[:]), nil
}
// asserts that an accessToken is a valid uuid
// IsValidAccessToken asserts that an accessToken is a valid uuid
func IsValidAccessToken(token string) bool {
_, err := uuid.Parse(token)
return err == nil

View File

@ -25,7 +25,7 @@ import (
"github.com/grafana/grafana/pkg/tsdb/legacydata"
)
// Define the Service Implementation. We're generating mock implementation
// PublicDashboardServiceImpl Define the Service Implementation. We're generating mock implementation
// automatically
type PublicDashboardServiceImpl struct {
log log.Logger
@ -43,7 +43,7 @@ var LogPrefix = "publicdashboards.service"
// the interface
var _ publicdashboards.Service = (*PublicDashboardServiceImpl)(nil)
// Factory for method used by wire to inject dependencies.
// ProvideService Factory for method used by wire to inject dependencies.
// builds the service, and api, and configures routes
func ProvideService(
cfg *setting.Cfg,
@ -63,7 +63,7 @@ func ProvideService(
}
}
// Gets a dashboard by Uid
// GetDashboard Gets a dashboard by Uid
func (pd *PublicDashboardServiceImpl) GetDashboard(ctx context.Context, dashboardUid string) (*models.Dashboard, error) {
dashboard, err := pd.store.GetDashboard(ctx, dashboardUid)
@ -74,7 +74,7 @@ func (pd *PublicDashboardServiceImpl) GetDashboard(ctx context.Context, dashboar
return dashboard, err
}
// Gets public dashboard via access token
// GetPublicDashboard Gets public dashboard via access token
func (pd *PublicDashboardServiceImpl) GetPublicDashboard(ctx context.Context, accessToken string) (*PublicDashboard, *models.Dashboard, error) {
pubdash, dash, err := pd.store.GetPublicDashboard(ctx, accessToken)
ctxLogger := pd.log.FromContext(ctx)