mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
	
	
		
			307 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Go
		
	
	
	
		
		
			
		
	
	
			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)
							 | 
						||
| 
								 | 
							
										})
							 | 
						||
| 
								 | 
							
									}
							 | 
						||
| 
								 | 
							
								}
							 |