live: add allowed_origins option (#36318)

This commit is contained in:
Alexander Emelin 2021-07-01 09:30:09 +03:00 committed by GitHub
parent b8010ba9f5
commit 483418dbb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 115 additions and 19 deletions

View File

@ -901,6 +901,10 @@ plugin_catalog_url = https://grafana.com/grafana/plugins/
# tuning. 0 disables Live, -1 means unlimited connections. # tuning. 0 disables Live, -1 means unlimited connections.
max_connections = 100 max_connections = 100
# allowed_origins is a comma-separated list of origins that can establish connection with Grafana Live.
# If not set then origin will be matched over root_url. Supports globbing: see https://github.com/gobwas/glob.
allowed_origins =
# engine defines an HA (high availability) engine to use for Grafana Live. By default no engine used - in # engine defines an HA (high availability) engine to use for Grafana Live. By default no engine used - in
# this case Live features work only on a single Grafana server. # this case Live features work only on a single Grafana server.
# Available options: "redis". # Available options: "redis".

View File

@ -887,6 +887,10 @@
# tuning. 0 disables Live, -1 means unlimited connections. # tuning. 0 disables Live, -1 means unlimited connections.
;max_connections = 100 ;max_connections = 100
# allowed_origins is a comma-separated list of origins that can establish connection with Grafana Live.
# If not set then origin will be matched over root_url. Supports globbing: see https://github.com/gobwas/glob.
;allowed_origins =
# engine defines an HA (high availability) engine to use for Grafana Live. By default no engine used - in # engine defines an HA (high availability) engine to use for Grafana Live. By default no engine used - in
# this case Live features work only on a single Grafana server. Available options: "redis". # this case Live features work only on a single Grafana server. Available options: "redis".
# Setting ha_engine is an EXPERIMENTAL feature. # Setting ha_engine is an EXPERIMENTAL feature.

View File

@ -10,6 +10,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/gobwas/glob"
"github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/api/routing"
@ -318,21 +320,21 @@ func (g *GrafanaLive) Init() error {
return fmt.Errorf("error parsing AppURL %s: %w", g.Cfg.AppURL, err) return fmt.Errorf("error parsing AppURL %s: %w", g.Cfg.AppURL, err)
} }
originPatterns := g.Cfg.LiveAllowedOrigins
originGlobs, _ := setting.GetAllowedOriginGlobs(originPatterns) // error already checked on config load.
checkOrigin := getCheckOriginFunc(appURL, originPatterns, originGlobs)
// Use a pure websocket transport. // Use a pure websocket transport.
wsHandler := centrifuge.NewWebsocketHandler(node, centrifuge.WebsocketConfig{ wsHandler := centrifuge.NewWebsocketHandler(node, centrifuge.WebsocketConfig{
ReadBufferSize: 1024, ReadBufferSize: 1024,
WriteBufferSize: 1024, WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { CheckOrigin: checkOrigin,
return checkOrigin(r, appURL)
},
}) })
pushWSHandler := pushws.NewHandler(g.ManagedStreamRunner, pushws.Config{ pushWSHandler := pushws.NewHandler(g.ManagedStreamRunner, pushws.Config{
ReadBufferSize: 1024, ReadBufferSize: 1024,
WriteBufferSize: 1024, WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool { CheckOrigin: checkOrigin,
return checkOrigin(r, appURL)
},
}) })
g.websocketHandler = func(ctx *models.ReqContext) { g.websocketHandler = func(ctx *models.ReqContext) {
@ -371,21 +373,44 @@ func (g *GrafanaLive) Init() error {
return nil return nil
} }
func checkOrigin(r *http.Request, appURL *url.URL) bool { func getCheckOriginFunc(appURL *url.URL, originPatterns []string, originGlobs []glob.Glob) func(r *http.Request) bool {
origin := r.Header.Get("Origin") return func(r *http.Request) bool {
if origin == "" { origin := r.Header.Get("Origin")
if origin == "" {
return true
}
if len(originPatterns) == 1 && originPatterns[0] == "*" {
// fast path for *.
return true
}
ok, err := checkAllowedOrigin(strings.ToLower(origin), appURL, originGlobs)
if err != nil {
logger.Warn("Error parsing request origin", "error", err, "origin", origin)
return false
}
if !ok {
logger.Warn("Request Origin is not authorized", "origin", origin, "appUrl", appURL.String(), "allowedOrigins", strings.Join(originPatterns, ","))
return false
}
return true return true
} }
}
func checkAllowedOrigin(origin string, appURL *url.URL, originGlobs []glob.Glob) (bool, error) {
originURL, err := url.Parse(origin) originURL, err := url.Parse(origin)
if err != nil { if err != nil {
logger.Warn("Failed to parse request origin", "error", err, "origin", origin) logger.Warn("Failed to parse request origin", "error", err, "origin", origin)
return false return false, err
} }
if !strings.EqualFold(originURL.Scheme, appURL.Scheme) || !strings.EqualFold(originURL.Host, appURL.Host) { if strings.EqualFold(originURL.Scheme, appURL.Scheme) && strings.EqualFold(originURL.Host, appURL.Host) {
logger.Warn("Request Origin is not authorized", "origin", origin, "appUrl", appURL.String()) return true, nil
return false
} }
return true for _, pattern := range originGlobs {
if pattern.Match(origin) {
return true, nil
}
}
return false, nil
} }
func runConcurrentlyIfNeeded(ctx context.Context, semaphore chan struct{}, fn func()) error { func runConcurrentlyIfNeeded(ctx context.Context, semaphore chan struct{}, fn func()) error {

View File

@ -7,6 +7,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -55,10 +57,11 @@ func Test_runConcurrentlyIfNeeded_DeadlineExceeded(t *testing.T) {
func TestCheckOrigin(t *testing.T) { func TestCheckOrigin(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
origin string origin string
appURL string appURL string
success bool allowedOrigins []string
success bool
}{ }{
{ {
name: "empty_origin", name: "empty_origin",
@ -96,6 +99,27 @@ func TestCheckOrigin(t *testing.T) {
appURL: "https://example.com", appURL: "https://example.com",
success: true, success: true,
}, },
{
name: "authorized_allowed_origins",
origin: "https://test.example.com",
appURL: "http://localhost:3000/",
allowedOrigins: []string{"https://test.example.com"},
success: true,
},
{
name: "authorized_allowed_origins_pattern",
origin: "https://test.example.com",
appURL: "http://localhost:3000/",
allowedOrigins: []string{"https://*.example.com"},
success: true,
},
{
name: "authorized_allowed_origins_all",
origin: "https://test.example.com",
appURL: "http://localhost:3000/",
allowedOrigins: []string{"*"},
success: true,
},
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -104,9 +128,15 @@ func TestCheckOrigin(t *testing.T) {
t.Parallel() t.Parallel()
appURL, err := url.Parse(tc.appURL) appURL, err := url.Parse(tc.appURL)
require.NoError(t, err) require.NoError(t, err)
originGlobs, err := setting.GetAllowedOriginGlobs(tc.allowedOrigins)
require.NoError(t, err)
checkOrigin := getCheckOriginFunc(appURL, tc.allowedOrigins, originGlobs)
r := httptest.NewRequest("GET", tc.appURL, nil) r := httptest.NewRequest("GET", tc.appURL, nil)
r.Header.Set("Origin", tc.origin) r.Header.Set("Origin", tc.origin)
require.Equal(t, tc.success, checkOrigin(r, appURL), require.Equal(t, tc.success, checkOrigin(r),
"origin %s, appURL: %s", tc.origin, tc.appURL, "origin %s, appURL: %s", tc.origin, tc.appURL,
) )
}) })

View File

@ -17,6 +17,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/gobwas/glob"
"github.com/prometheus/common/model" "github.com/prometheus/common/model"
ini "gopkg.in/ini.v1" ini "gopkg.in/ini.v1"
@ -391,6 +393,9 @@ type Cfg struct {
LiveHAEngine string LiveHAEngine string
// LiveHAEngineAddress is a connection address for Live HA engine. // LiveHAEngineAddress is a connection address for Live HA engine.
LiveHAEngineAddress string LiveHAEngineAddress string
// LiveAllowedOrigins is a set of origins accepted by Live. If not provided
// then Live uses AppURL as the only allowed origin.
LiveAllowedOrigins []string
// Grafana.com URL // Grafana.com URL
GrafanaComURL string GrafanaComURL string
@ -1446,6 +1451,19 @@ func (cfg *Cfg) readDataSourcesSettings() {
cfg.DataSourceLimit = datasources.Key("datasource_limit").MustInt(5000) cfg.DataSourceLimit = datasources.Key("datasource_limit").MustInt(5000)
} }
func GetAllowedOriginGlobs(originPatterns []string) ([]glob.Glob, error) {
var originGlobs []glob.Glob
allowedOrigins := originPatterns
for _, originPattern := range allowedOrigins {
g, err := glob.Compile(originPattern)
if err != nil {
return nil, fmt.Errorf("error parsing origin pattern: %v", err)
}
originGlobs = append(originGlobs, g)
}
return originGlobs, nil
}
func (cfg *Cfg) readLiveSettings(iniFile *ini.File) error { func (cfg *Cfg) readLiveSettings(iniFile *ini.File) error {
section := iniFile.Section("live") section := iniFile.Section("live")
cfg.LiveMaxConnections = section.Key("max_connections").MustInt(100) cfg.LiveMaxConnections = section.Key("max_connections").MustInt(100)
@ -1459,5 +1477,20 @@ func (cfg *Cfg) readLiveSettings(iniFile *ini.File) error {
return fmt.Errorf("unsupported live HA engine type: %s", cfg.LiveHAEngine) return fmt.Errorf("unsupported live HA engine type: %s", cfg.LiveHAEngine)
} }
cfg.LiveHAEngineAddress = section.Key("ha_engine_address").MustString("127.0.0.1:6379") cfg.LiveHAEngineAddress = section.Key("ha_engine_address").MustString("127.0.0.1:6379")
var originPatterns []string
allowedOrigins := section.Key("allowed_origins").MustString("")
for _, originPattern := range strings.Split(allowedOrigins, ",") {
originPattern = strings.TrimSpace(originPattern)
if originPattern == "" {
continue
}
originPatterns = append(originPatterns, originPattern)
}
_, err := GetAllowedOriginGlobs(originPatterns)
if err != nil {
return err
}
cfg.LiveAllowedOrigins = originPatterns
return nil return nil
} }