Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add a middleware to verify the project exists during a request #680

Merged
merged 6 commits into from
Oct 25, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
add a middleware to verify the project exists during a request
Signed-off-by: Augustin Husson <husson.augustin@gmail.com>
  • Loading branch information
Nexucis committed Oct 24, 2022
commit f85a7a1e4273b6cfa80c68cd104d38fa35e67e49
3 changes: 2 additions & 1 deletion cmd/perses/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ func main() {
runner.HTTPServerBuilder().
APIRegistration(persesAPI).
APIRegistration(persesFrontend).
Middleware(middleware.Proxy(persistenceManager.GetDatasource(), persistenceManager.GetGlobalDatasource()))
Middleware(middleware.Proxy(persistenceManager.GetDatasource(), persistenceManager.GetGlobalDatasource())).
Middleware(middleware.CheckProject(persistenceManager.GetProject()))

// start the application
runner.Start()
Expand Down
3 changes: 1 addition & 2 deletions internal/api/core/middleware/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ type proxy interface {
}

func newProxy(spec v1.DatasourceSpec, path string) (proxy, error) {
cfg, err := datasourceHTTP.CheckAndValidate(spec.Plugin.Spec)
cfg, err := datasourceHTTP.ValidateAndExtract(spec.Plugin.Spec)
if err != nil {
logrus.WithError(err).Error("unable to build or find the http config in the datasource")
return nil, echo.NewHTTPError(http.StatusBadGateway, "unable to find the http config")
Expand All @@ -137,7 +137,6 @@ func newProxy(spec v1.DatasourceSpec, path string) (proxy, error) {
path: path,
}, nil
}
// TODO build the HTTP proxy
return nil, echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("datasource type '%T' not managed", spec))
}

Expand Down
72 changes: 72 additions & 0 deletions internal/api/core/middleware/verification.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2022 The Perses Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package middleware

import (
"errors"
"fmt"
"net/http"
"strings"

"github.com/labstack/echo/v4"
"github.com/perses/perses/internal/api/interface/v1/project"
"github.com/perses/perses/internal/api/shared"
)

type partialMetadata struct {
Project string `json:"project"`
}

type partialObject struct {
Metadata partialMetadata `json:"metadata"`
}

// CheckProject is a middleware that will verify if the project used for the request exists.
func CheckProject(dao project.DAO) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
projectName := shared.GetProjectParameter(c)
if len(projectName) == 0 {
// It's possible the HTTP Path doesn't contain the project because the user is calling the root endpoint
// to create a new dashboard for example.
// So we need to ensure the project name exists in the resource, which is why we will partially decode the body to get the project name.
// And just to avoid a non-necessary deserialization, we will ensure we are managing a resource that is part of a project by checking the HTTP Path.
if c.Request().Method == http.MethodPost {
for _, path := range shared.ProjectResourcePathList {
if strings.HasPrefix(c.Path(), fmt.Sprintf("%s/%s", shared.APIV1Prefix, path)) {
o := &partialObject{}
if err := c.Bind(o); err != nil {
return err
}
if len(o.Metadata.Project) == 0 {
return shared.HandleError(fmt.Errorf("%w: metadata.project cannot be empty", shared.BadRequestError))
}
projectName = o.Metadata.Project
break
}
}
}
}
if len(projectName) > 0 {
if _, err := dao.Get(projectName); err != nil {
if errors.Is(err, shared.NotFoundError) {
return shared.HandleError(fmt.Errorf("%w, metadata.project %q doesn't exist", shared.BadRequestError, projectName))
}
return shared.HandleError(err)
}
}
return next(c)
}
}
}
38 changes: 36 additions & 2 deletions internal/api/e2e/datasource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func TestCreateDatasourceWithConflict(t *testing.T) {
}

func TestCreateDatasourceBadRequest(t *testing.T) {
project := &v1.Datasource{Kind: v1.KindDatasource}
dts := &v1.Datasource{Kind: v1.KindDatasource}

server, _ := utils.CreateServer(t)
defer server.Close()
Expand All @@ -80,7 +80,41 @@ func TestCreateDatasourceBadRequest(t *testing.T) {

// metadata.name is not provided, it should return a bad request
e.POST(fmt.Sprintf("%s/%s", shared.APIV1Prefix, shared.PathDatasource)).
WithJSON(project).
WithJSON(dts).
Expect().
Status(http.StatusBadRequest)
}

func TestCreateDatasourceWithEmptyProjectName(t *testing.T) {
dts := &v1.Datasource{Kind: v1.KindDatasource}
dts.Metadata.Project = ""
server, _ := utils.CreateServer(t)
defer server.Close()
e := httpexpect.WithConfig(httpexpect.Config{
BaseURL: server.URL,
Reporter: httpexpect.NewAssertReporter(t),
})

// metadata.name is not provided, it should return a bad request
e.POST(fmt.Sprintf("%s/%s", shared.APIV1Prefix, shared.PathDatasource)).
WithJSON(dts).
Expect().
Status(http.StatusBadRequest)
}

func TestCreateDatasourceWithNonExistingProject(t *testing.T) {
dts := &v1.Datasource{Kind: v1.KindDatasource}
dts.Metadata.Project = "404NotFound"
server, _ := utils.CreateServer(t)
defer server.Close()
e := httpexpect.WithConfig(httpexpect.Config{
BaseURL: server.URL,
Reporter: httpexpect.NewAssertReporter(t),
})

// metadata.name is not provided, it should return a bad request
e.POST(fmt.Sprintf("%s/%s", shared.APIV1Prefix, shared.PathDatasource)).
WithJSON(dts).
Expect().
Status(http.StatusBadRequest)
}
Expand Down
3 changes: 0 additions & 3 deletions internal/api/impl/v1/dashboard/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,6 @@ func (s *service) Create(entity api.Entity) (interface{}, error) {
}

func (s *service) create(entity *v1.Dashboard) (*v1.Dashboard, error) {
// Note: you don't need to check that the project exists since once the permission middleware will be in place,
// it won't be possible to create a resources into a not known project

// verify this new dashboard passes the validation
if err := validate.Dashboard(entity, s.sch); err != nil {
return nil, fmt.Errorf("%w: %s", shared.BadRequestError, err)
Expand Down
2 changes: 1 addition & 1 deletion internal/api/shared/toolbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ type Parameters struct {

func extractParameters(ctx echo.Context) Parameters {
return Parameters{
Project: getProjectParameter(ctx),
Project: GetProjectParameter(ctx),
Name: getNameParameter(ctx),
}
}
Expand Down
7 changes: 6 additions & 1 deletion internal/api/shared/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,16 @@ const (
PathUser = "users"
)

// ProjectResourcePathList is containing the list of the resource path that are part of a project.
var ProjectResourcePathList = []string{
PathDashboard, PathDatasource, PathFolder,
}

func getNameParameter(ctx echo.Context) string {
return ctx.Param(ParamName)
}

func getProjectParameter(ctx echo.Context) string {
func GetProjectParameter(ctx echo.Context) string {
return ctx.Param(ParamProject)
}

Expand Down
2 changes: 1 addition & 1 deletion internal/api/shared/validate/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func Dashboard(entity *modelV1.Dashboard, sch schemas.Schemas) error {

func Datasource[T modelV1.DatasourceInterface](entity T, list []T, sch schemas.Schemas) error {
plugin := entity.GetSpec().Plugin
if _, err := http.CheckAndValidate(plugin.Spec); err != nil {
if _, err := http.ValidateAndExtract(plugin.Spec); err != nil {
return err
}
if list != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/model/api/v1/datasource/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ const (
httpProxySpec = "spec"
)

func CheckAndValidate(pluginSpec interface{}) (*Config, error) {
func ValidateAndExtract(pluginSpec interface{}) (*Config, error) {
finder := &configFinder{}
finder.find(reflect.ValueOf(pluginSpec))
return finder.config, finder.err
Expand Down
6 changes: 3 additions & 3 deletions pkg/model/api/v1/datasource/http/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ method: POST
}
}

func TestCheckAndValidate(t *testing.T) {
func TestValidateAndExtract(t *testing.T) {
// Check and Validate HTTPProxy contained in a proper struct
type aStruct struct {
A struct {
Expand All @@ -444,7 +444,7 @@ func TestCheckAndValidate(t *testing.T) {
}{Kind: "HTTPProxy", Spec: &Config{URL: u}}},
}

c, err := CheckAndValidate(b)
c, err := ValidateAndExtract(b)
assert.NoError(t, err)
assert.Equal(t, &Config{URL: u}, c)

Expand All @@ -458,7 +458,7 @@ func TestCheckAndValidate(t *testing.T) {
},
},
}
c, err = CheckAndValidate(uglyStruct)
c, err = ValidateAndExtract(uglyStruct)
assert.NoError(t, err)
assert.Equal(t, &Config{URL: u}, c)
}