mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
API: Recognize MSSQL data source URLs (#25629)
* API: Recognize MSSQL URLs Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com> * Move MSSQL URL validation into mssql package Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
This commit is contained in:
parent
e6a5e88054
commit
d352c213b3
@ -4,46 +4,59 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb/mssql"
|
||||||
)
|
)
|
||||||
|
|
||||||
var logger = log.New("datasource")
|
var logger = log.New("datasource")
|
||||||
|
|
||||||
// URLValidationError represents an error from validating a data source URL.
|
// URLValidationError represents an error from validating a data source URL.
|
||||||
type URLValidationError struct {
|
type URLValidationError struct {
|
||||||
error
|
Err error
|
||||||
|
|
||||||
url string
|
URL string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error returns the error message.
|
// Error returns the error message.
|
||||||
func (e URLValidationError) Error() string {
|
func (e URLValidationError) Error() string {
|
||||||
return fmt.Sprintf("Validation of data source URL %q failed: %s", e.url, e.error.Error())
|
return fmt.Sprintf("Validation of data source URL %q failed: %s", e.URL, e.Err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unwrap returns the wrapped error.
|
// Unwrap returns the wrapped error.
|
||||||
func (e URLValidationError) Unwrap() error {
|
func (e URLValidationError) Unwrap() error {
|
||||||
return e.error
|
return e.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
// reURL is a regexp to detect if a URL specifies the protocol. We match also strings where the actual protocol is
|
// reURL is a regexp to detect if a URL specifies the protocol. We match also strings where the actual protocol is
|
||||||
// missing (i.e., "://"), in order to catch these as invalid when parsing.
|
// missing (i.e., "://"), in order to catch these as invalid when parsing.
|
||||||
var reURL = regexp.MustCompile("^[^:]*://")
|
var reURL = regexp.MustCompile("^[^:]*://")
|
||||||
|
|
||||||
// ValidateURL validates a data source URL.
|
// ValidateURL validates a data source's URL.
|
||||||
//
|
//
|
||||||
// If successful, the valid URL object is returned, otherwise an error is returned.
|
// The data source's type and URL must be provided. If successful, the valid URL object is returned, otherwise an
|
||||||
func ValidateURL(urlStr string) (*url.URL, error) {
|
// error is returned.
|
||||||
// Make sure the URL starts with a protocol specifier, so parsing is unambiguous
|
func ValidateURL(typeName, urlStr string) (*url.URL, error) {
|
||||||
if !reURL.MatchString(urlStr) {
|
var u *url.URL
|
||||||
logger.Debug(
|
var err error
|
||||||
"Data source URL doesn't specify protocol, so prepending it with http:// in order to make it unambiguous")
|
switch strings.ToLower(typeName) {
|
||||||
urlStr = fmt.Sprintf("http://%s", urlStr)
|
case "mssql":
|
||||||
|
u, err = mssql.ParseURL(urlStr)
|
||||||
|
default:
|
||||||
|
logger.Debug("Applying default URL parsing for this data source type", "type", typeName, "url", urlStr)
|
||||||
|
|
||||||
|
// Make sure the URL starts with a protocol specifier, so parsing is unambiguous
|
||||||
|
if !reURL.MatchString(urlStr) {
|
||||||
|
logger.Debug(
|
||||||
|
"Data source URL doesn't specify protocol, so prepending it with http:// in order to make it unambiguous",
|
||||||
|
"type", typeName, "url", urlStr)
|
||||||
|
urlStr = fmt.Sprintf("http://%s", urlStr)
|
||||||
|
}
|
||||||
|
u, err = url.Parse(urlStr)
|
||||||
}
|
}
|
||||||
u, err := url.Parse(urlStr)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, URLValidationError{error: err, url: urlStr}
|
return nil, URLValidationError{Err: err, URL: urlStr}
|
||||||
}
|
}
|
||||||
|
|
||||||
return u, nil
|
return u, nil
|
||||||
|
@ -9,12 +9,15 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/api/datasource"
|
"github.com/grafana/grafana/pkg/api/datasource"
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/plugins"
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/plugins/datasource/wrapper"
|
"github.com/grafana/grafana/pkg/plugins/datasource/wrapper"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var datasourcesLogger = log.New("datasources")
|
||||||
|
|
||||||
func GetDataSources(c *models.ReqContext) Response {
|
func GetDataSources(c *models.ReqContext) Response {
|
||||||
query := models.GetDataSourcesQuery{OrgId: c.OrgId}
|
query := models.GetDataSourcesQuery{OrgId: c.OrgId}
|
||||||
|
|
||||||
@ -127,9 +130,11 @@ func DeleteDataSourceByName(c *models.ReqContext) Response {
|
|||||||
return Success("Data source deleted")
|
return Success("Data source deleted")
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateURL(u string) Response {
|
func validateURL(tp string, u string) Response {
|
||||||
if u != "" {
|
if u != "" {
|
||||||
if _, err := datasource.ValidateURL(u); err != nil {
|
if _, err := datasource.ValidateURL(tp, u); err != nil {
|
||||||
|
datasourcesLogger.Error("Received invalid data source URL as part of data source command",
|
||||||
|
"url", u)
|
||||||
return Error(400, fmt.Sprintf("Validation error, invalid URL: %q", u), err)
|
return Error(400, fmt.Sprintf("Validation error, invalid URL: %q", u), err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -138,8 +143,9 @@ func validateURL(u string) Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func AddDataSource(c *models.ReqContext, cmd models.AddDataSourceCommand) Response {
|
func AddDataSource(c *models.ReqContext, cmd models.AddDataSourceCommand) Response {
|
||||||
|
datasourcesLogger.Debug("Received command to add data source", "url", cmd.Url)
|
||||||
cmd.OrgId = c.OrgId
|
cmd.OrgId = c.OrgId
|
||||||
if resp := validateURL(cmd.Url); resp != nil {
|
if resp := validateURL(cmd.Type, cmd.Url); resp != nil {
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,9 +167,10 @@ func AddDataSource(c *models.ReqContext, cmd models.AddDataSourceCommand) Respon
|
|||||||
}
|
}
|
||||||
|
|
||||||
func UpdateDataSource(c *models.ReqContext, cmd models.UpdateDataSourceCommand) Response {
|
func UpdateDataSource(c *models.ReqContext, cmd models.UpdateDataSourceCommand) Response {
|
||||||
|
datasourcesLogger.Debug("Received command to update data source", "url", cmd.Url)
|
||||||
cmd.OrgId = c.OrgId
|
cmd.OrgId = c.OrgId
|
||||||
cmd.Id = c.ParamsInt64(":id")
|
cmd.Id = c.ParamsInt64(":id")
|
||||||
if resp := validateURL(cmd.Url); resp != nil {
|
if resp := validateURL(cmd.Type, cmd.Url); resp != nil {
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ func (lw *logWrapper) Write(p []byte) (n int, err error) {
|
|||||||
// NewDataSourceProxy creates a new Datasource proxy
|
// NewDataSourceProxy creates a new Datasource proxy
|
||||||
func NewDataSourceProxy(ds *models.DataSource, plugin *plugins.DataSourcePlugin, ctx *models.ReqContext,
|
func NewDataSourceProxy(ds *models.DataSource, plugin *plugins.DataSourcePlugin, ctx *models.ReqContext,
|
||||||
proxyPath string, cfg *setting.Cfg) (*DataSourceProxy, error) {
|
proxyPath string, cfg *setting.Cfg) (*DataSourceProxy, error) {
|
||||||
targetURL, err := datasource.ValidateURL(ds.Url)
|
targetURL, err := datasource.ValidateURL(ds.Type, ds.Url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/api/datasource"
|
||||||
"github.com/grafana/grafana/pkg/components/securejsondata"
|
"github.com/grafana/grafana/pkg/components/securejsondata"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@ -583,11 +584,62 @@ func TestNewDataSourceProxy_ProtocolLessURL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
cfg := setting.Cfg{}
|
cfg := setting.Cfg{}
|
||||||
plugin := plugins.DataSourcePlugin{}
|
plugin := plugins.DataSourcePlugin{}
|
||||||
|
|
||||||
_, err := NewDataSourceProxy(&ds, &plugin, &ctx, "api/method", &cfg)
|
_, err := NewDataSourceProxy(&ds, &plugin, &ctx, "api/method", &cfg)
|
||||||
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test wth MSSQL type data sources.
|
||||||
|
func TestNewDataSourceProxy_MSSQL(t *testing.T) {
|
||||||
|
ctx := models.ReqContext{
|
||||||
|
Context: &macaron.Context{
|
||||||
|
Req: macaron.Request{},
|
||||||
|
},
|
||||||
|
SignedInUser: &models.SignedInUser{OrgRole: models.ROLE_EDITOR},
|
||||||
|
}
|
||||||
|
tcs := []struct {
|
||||||
|
description string
|
||||||
|
url string
|
||||||
|
err error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
description: "Valid ODBC URL",
|
||||||
|
url: `localhost\instance:1433`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
description: "Invalid ODBC URL",
|
||||||
|
url: `localhost\instance::1433`,
|
||||||
|
err: datasource.URLValidationError{
|
||||||
|
Err: fmt.Errorf(`unrecognized MSSQL URL format: "localhost\\instance::1433"`),
|
||||||
|
URL: `localhost\instance::1433`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range tcs {
|
||||||
|
t.Run(tc.description, func(t *testing.T) {
|
||||||
|
cfg := setting.Cfg{}
|
||||||
|
plugin := plugins.DataSourcePlugin{}
|
||||||
|
ds := models.DataSource{
|
||||||
|
Type: "mssql",
|
||||||
|
Url: tc.url,
|
||||||
|
}
|
||||||
|
|
||||||
|
p, err := NewDataSourceProxy(&ds, &plugin, &ctx, "api/method", &cfg)
|
||||||
|
if tc.err == nil {
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, &url.URL{
|
||||||
|
Scheme: "sqlserver",
|
||||||
|
Host: ds.Url,
|
||||||
|
}, p.targetUrl)
|
||||||
|
} else {
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Equal(t, tc.err, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type CloseNotifierResponseRecorder struct {
|
type CloseNotifierResponseRecorder struct {
|
||||||
*httptest.ResponseRecorder
|
*httptest.ResponseRecorder
|
||||||
closeChan chan bool
|
closeChan chan bool
|
||||||
|
@ -3,17 +3,18 @@ package mssql
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
|
||||||
_ "github.com/denisenkom/go-mssqldb"
|
_ "github.com/denisenkom/go-mssqldb"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/tsdb"
|
"github.com/grafana/grafana/pkg/tsdb"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/sqleng"
|
"github.com/grafana/grafana/pkg/tsdb/sqleng"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
|
||||||
"github.com/grafana/grafana/pkg/util/errutil"
|
|
||||||
"xorm.io/core"
|
"xorm.io/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -21,9 +22,9 @@ func init() {
|
|||||||
tsdb.RegisterTsdbQueryEndpoint("mssql", newMssqlQueryEndpoint)
|
tsdb.RegisterTsdbQueryEndpoint("mssql", newMssqlQueryEndpoint)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
|
var logger = log.New("tsdb.mssql")
|
||||||
logger := log.New("tsdb.mssql")
|
|
||||||
|
|
||||||
|
func newMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
|
||||||
cnnstr, err := generateConnectionString(datasource)
|
cnnstr, err := generateConnectionString(datasource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -46,12 +47,46 @@ func newMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin
|
|||||||
return sqleng.NewSqlQueryEndpoint(&config, &queryResultTransformer, newMssqlMacroEngine(), logger)
|
return sqleng.NewSqlQueryEndpoint(&config, &queryResultTransformer, newMssqlMacroEngine(), logger)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ParseURL tries to parse an MSSQL URL string into a URL object.
|
||||||
|
func ParseURL(u string) (*url.URL, error) {
|
||||||
|
logger.Debug("Parsing MSSQL URL", "url", u)
|
||||||
|
|
||||||
|
// Recognize ODBC connection strings like host\instance:1234
|
||||||
|
reODBC := regexp.MustCompile(`^[^\\:]+(?:\\[^:]+)?(?::\d+)?$`)
|
||||||
|
var host string
|
||||||
|
switch {
|
||||||
|
case reODBC.MatchString(u):
|
||||||
|
logger.Debug("Recognized as ODBC URL format", "url", u)
|
||||||
|
host = u
|
||||||
|
default:
|
||||||
|
logger.Debug("Couldn't recognize as valid MSSQL URL", "url", u)
|
||||||
|
return nil, fmt.Errorf("unrecognized MSSQL URL format: %q", u)
|
||||||
|
}
|
||||||
|
return &url.URL{
|
||||||
|
Scheme: "sqlserver",
|
||||||
|
Host: host,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func generateConnectionString(datasource *models.DataSource) (string, error) {
|
func generateConnectionString(datasource *models.DataSource) (string, error) {
|
||||||
addr, err := util.SplitHostPortDefault(datasource.Url, "localhost", "1433")
|
var addr util.NetworkAddress
|
||||||
if err != nil {
|
if datasource.Url != "" {
|
||||||
return "", errutil.Wrapf(err, "Invalid data source URL '%s'", datasource.Url)
|
u, err := ParseURL(datasource.Url)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
addr, err = util.SplitHostPortDefault(u.Host, "localhost", "1433")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addr = util.NetworkAddress{
|
||||||
|
Host: "localhost",
|
||||||
|
Port: "1433",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Debug("Generating connection string", "url", datasource.Url, "host", addr.Host, "port", addr.Port)
|
||||||
encrypt := datasource.JsonData.Get("encrypt").MustString("false")
|
encrypt := datasource.JsonData.Get("encrypt").MustString("false")
|
||||||
connStr := fmt.Sprintf("server=%s;port=%s;database=%s;user id=%s;password=%s;",
|
connStr := fmt.Sprintf("server=%s;port=%s;database=%s;user id=%s;password=%s;",
|
||||||
addr.Host,
|
addr.Host,
|
||||||
|
Loading…
Reference in New Issue
Block a user