grafana/pkg/middleware/validate_action_url_test.go
Adela Almasan f64b121ddb
Canvas: Allow API calls to grafana origin (#91822)
* allow post URL
* check for config
* allow relative paths
* add allowed internal pattern; add checks for method
* update defaults.ini
* add custom header
* update config comment
* use globbing, switch to older middleware - deprecated call
* add codeowner
* update to use current api, add test
* update fall through logic

* Update pkg/middleware/validate_action_url.go

Co-authored-by: Dan Cech <dcech@grafana.com>

* Update pkg/middleware/validate_action_url.go

Co-authored-by: Dan Cech <dcech@grafana.com>

* add more tests

* Update pkg/middleware/validate_action_url_test.go

Co-authored-by: Dan Cech <dcech@grafana.com>

* fix request headers

* add additional tests for all verbs

* fix request headers++

* throw error when method is unknown

---------

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
Co-authored-by: Brian Gann <bkgann@gmail.com>
Co-authored-by: Brian Gann <briangann@users.noreply.github.com>
Co-authored-by: Dan Cech <dcech@grafana.com>
2024-09-10 10:45:27 -04:00

307 lines
8.6 KiB
Go

package middleware
import (
"fmt"
"net/http"
"testing"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
)
func TestMiddlewareValidateActionUrl(t *testing.T) {
tests := []struct {
name string
method string
path string
actionsAllowPostURL string
addHeader bool
code int
}{
{
name: "DELETE action with valid path",
method: "DELETE",
path: "/api/plugins/org-generic-app",
actionsAllowPostURL: "/api/plugins/*",
addHeader: true,
code: http.StatusMethodNotAllowed,
},
{
name: "DELETE action with invalid path",
method: "DELETE",
path: "/api/notplugins/org-generic-app",
actionsAllowPostURL: "/api/plugins/*",
addHeader: true,
code: http.StatusMethodNotAllowed,
},
{
name: "GET action with valid path",
method: "GET",
path: "/api/plugins/org-generic-app",
actionsAllowPostURL: "/api/plugins/*",
addHeader: true,
code: http.StatusMethodNotAllowed,
},
{
name: "GET action with invalid path",
method: "GET",
path: "/api/notplugins/org-generic-app",
actionsAllowPostURL: "/api/plugins/*",
addHeader: true,
code: http.StatusMethodNotAllowed,
},
{
name: "GET valid path without header",
method: "GET",
path: "/", // top-level get
actionsAllowPostURL: "",
addHeader: false,
code: http.StatusOK,
},
{
name: "GET valid path with header",
method: "GET",
path: "/", // top-level get
actionsAllowPostURL: "",
addHeader: true,
code: http.StatusMethodNotAllowed,
},
{
name: "HEAD request with header",
method: "HEAD",
path: "/", // top-level
actionsAllowPostURL: "",
addHeader: true,
code: http.StatusMethodNotAllowed,
},
{
name: "OPTIONS request",
method: "OPTIONS",
path: "/", // top-level
actionsAllowPostURL: "",
addHeader: false,
code: http.StatusOK,
},
{
name: "OPTIONS request with header",
method: "OPTIONS",
path: "/", // top-level
actionsAllowPostURL: "",
addHeader: true,
code: http.StatusMethodNotAllowed,
},
{
name: "PATCH request with header",
method: "PATCH",
path: "/", // top-level
actionsAllowPostURL: "",
addHeader: true,
code: http.StatusMethodNotAllowed,
},
{
name: "PATCH request without header",
method: "PATCH",
path: "/", // top-level
actionsAllowPostURL: "",
addHeader: false,
code: http.StatusOK,
},
{
name: "POST without action header",
method: "POST",
path: "/api/plugins/org-generic-app",
actionsAllowPostURL: "",
addHeader: false,
code: http.StatusOK,
},
{
name: "POST with action header, no paths defined",
method: "POST",
path: "/api/plugins/org-generic-app",
actionsAllowPostURL: "",
addHeader: true,
code: http.StatusMethodNotAllowed,
},
{
name: "POST action with allowed path",
method: "POST",
path: "/api/plugins/org-generic-app",
actionsAllowPostURL: "/api/plugins/*",
addHeader: true,
code: http.StatusOK,
},
{
name: "POST action with invalid path",
method: "POST",
path: "/api/notplugins/org-generic-app",
actionsAllowPostURL: "/api/plugins/*",
addHeader: true,
code: http.StatusMethodNotAllowed,
},
{
name: "PUT action with valid path with header",
method: "PUT",
path: "/api/plugins/org-generic-app",
actionsAllowPostURL: "/api/plugins/*",
addHeader: true,
code: http.StatusOK,
},
{
name: "PUT action with invalid path",
method: "PUT",
path: "/api/notplugins/org-generic-app",
actionsAllowPostURL: "/api/plugins/*",
addHeader: true,
code: http.StatusMethodNotAllowed,
},
{
name: "PUT action with valid path without header",
method: "PUT",
path: "/api/plugins/org-generic-app",
actionsAllowPostURL: "/api/plugins/*",
addHeader: false,
code: http.StatusOK,
},
{
name: "PUT action with invalid path without header",
method: "PUT",
path: "/api/notplugins/org-generic-app",
actionsAllowPostURL: "/api/plugins/*",
addHeader: false,
code: http.StatusOK,
},
{
name: "CONNECT unknown verb with header",
method: "CONNECT",
path: "/api/notplugins/org-generic-app",
actionsAllowPostURL: "/api/plugins/*",
addHeader: true,
code: http.StatusMethodNotAllowed,
},
{
name: "CONNECT unknown verb without header",
method: "CONNECT",
path: "/api/notplugins/org-generic-app",
actionsAllowPostURL: "/api/plugins/*",
addHeader: false,
code: http.StatusNotFound,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
middlewareScenario(t, tt.name, func(t *testing.T, sc *scenarioContext) {
switch tt.method {
case "DELETE":
sc.m.Delete(tt.path, sc.defaultHandler)
case "GET":
sc.m.Get(tt.path, sc.defaultHandler)
case "HEAD":
sc.m.Head(tt.path, sc.defaultHandler)
case "OPTIONS":
sc.m.Options(tt.path, sc.defaultHandler)
case "PATCH":
sc.m.Patch(tt.path, sc.defaultHandler)
case "POST":
sc.m.Post(tt.path, sc.defaultHandler)
case "PUT":
sc.m.Put(tt.path, sc.defaultHandler)
default:
// anything else is an error
anError := fmt.Errorf("unknown verb: %s", tt.method)
if assert.Errorf(t, anError, "unknown verb: %s", tt.method) {
assert.Contains(t, anError.Error(), "unknown verb")
}
}
sc.fakeReq(tt.method, tt.path)
if tt.addHeader {
sc.req.Header.Add("X-Grafana-Action", "1")
}
sc.exec()
resp := sc.resp.Result()
t.Cleanup(func() {
err := resp.Body.Close()
assert.NoError(t, err)
})
// nolint:bodyclose
assert.Equal(t, tt.code, sc.resp.Result().StatusCode)
}, func(cfg *setting.Cfg) {
cfg.ActionsAllowPostURL = tt.actionsAllowPostURL
})
})
}
}
func TestMatchesAllowedPath(t *testing.T) {
tests := []struct {
name string
aPath string
allowList string
matches bool
}{
{
name: "single url with match",
allowList: "/api/plugins/*",
aPath: "/api/plugins/my-plugin",
matches: true,
},
{
name: "single url no match",
allowList: "/api/plugins/*",
aPath: "/api/plugin/my-plugin",
matches: false,
},
{
name: "multiple urls with match",
allowList: "/api/plugins/*, /api/other/**",
aPath: "/api/other/my-plugin",
matches: true,
},
{
name: "multiple urls no match",
allowList: "/api/plugins/*, /api/other/**",
aPath: "/api/misc/my-plugin",
matches: false,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
allGlobs, err := cacheGlobs(tc.allowList)
matched := matchesAllowedPath(allGlobs, tc.aPath)
assert.NoError(t, err)
assert.Equal(t, matched, tc.matches)
})
}
}
func TestCacheGlobs(t *testing.T) {
tests := []struct {
name string
allowList string
expectedLength int
}{
{
name: "single url",
allowList: "/api/plugins",
expectedLength: 1,
},
{
name: "multiple urls",
allowList: "/api/plugins, /api/other/**",
expectedLength: 2,
},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
cache, err := cacheGlobs(tc.allowList)
assert.NoError(t, err)
assert.Equal(t, len(*cache), tc.expectedLength)
})
}
}