The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/middleware/validate_action_url.go

119 lines
3.3 KiB

package middleware
import (
"fmt"
"net/http"
"github.com/gobwas/glob"
"github.com/grafana/grafana/pkg/cmd/grafana-cli/logger"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/contexthandler"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
var (
errInvalidAllowedURL = func(url string) error {
return fmt.Errorf("action URL '%s' is invalid", url)
}
)
type errorWithStatus struct {
Underlying error
HTTPStatus int
}
func (e errorWithStatus) Error() string {
return e.Underlying.Error()
}
func (e errorWithStatus) Unwrap() error {
return e.Underlying
}
func ValidateActionUrl(cfg *setting.Cfg, logger log.Logger) func(http.Handler) http.Handler {
// get the urls allowed from server config
allGlobs, globErr := cacheGlobs(cfg.ActionsAllowPostURL)
if globErr != nil {
logger.Error("invalid glob settings in config section [security] actions_allow_post_url", "url", cfg.ActionsAllowPostURL)
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := contexthandler.FromContext(r.Context())
// if no header
// return nil
// check if action header exists
action := ctx.Req.Header.Get("X-Grafana-Action")
if action == "" {
// header not found, this is not an action request
next.ServeHTTP(w, r)
return
}
if globErr != nil {
http.Error(w, "check server logs for glob configuration failure", http.StatusInternalServerError)
return
}
matchErr := check(ctx, allGlobs, logger)
if matchErr != nil {
http.Error(w, matchErr.Error(), http.StatusMethodNotAllowed)
return
}
// no errors fall through
next.ServeHTTP(w, r)
})
}
}
// check
// Detects header for action urls and compares to globbed pattern list
// returns true if allowed
func check(ctx *contextmodel.ReqContext, allGlobs *[]glob.Glob, logger log.Logger) *errorWithStatus {
// only process POST and PUT
if ctx.Req.Method != http.MethodPost && ctx.Req.Method != http.MethodPut {
return &errorWithStatus{
Underlying: fmt.Errorf("method not allowed for path %s", ctx.Req.URL),
HTTPStatus: http.StatusMethodNotAllowed,
}
}
// for each split config
// if matches glob
// return nil
urlToCheck := ctx.Req.URL
if matchesAllowedPath(allGlobs, urlToCheck.Path) {
return nil
}
logger.Warn("POST/PUT to path not allowed", "url", urlToCheck)
// return some error
return &errorWithStatus{
Underlying: fmt.Errorf("method POST/PUT not allowed for path %s", urlToCheck),
HTTPStatus: http.StatusMethodNotAllowed,
}
}
func matchesAllowedPath(allGlobs *[]glob.Glob, pathToCheck string) bool {
logger.Debug("Checking url", "actions", pathToCheck)
for _, rule := range *allGlobs {
logger.Debug("Checking match", "actions", rule)
if rule.Match(pathToCheck) {
// allowed
logger.Debug("POST/PUT call matches allow configuration settings")
return true
}
}
return false
}
func cacheGlobs(actionsAllowPostURL string) (*[]glob.Glob, error) {
allowedUrls := util.SplitString(actionsAllowPostURL)
allGlobs := make([]glob.Glob, 0)
for _, i := range allowedUrls {
g, err := glob.Compile(i)
if err != nil {
return nil, errInvalidAllowedURL(err.Error())
}
allGlobs = append(allGlobs, g)
}
return &allGlobs, nil
}