Jelajahi Sumber

InfluxDB now works in proxy mode, influxdb username and password is added in the backend and never exposed to frontend, #8

Torkel Ödegaard 11 tahun lalu
induk
melakukan
164d11c816

+ 123 - 0
pkg/api/account.go

@@ -0,0 +1,123 @@
+package api
+
+import (
+	"github.com/torkelo/grafana-pro/pkg/bus"
+	"github.com/torkelo/grafana-pro/pkg/middleware"
+	m "github.com/torkelo/grafana-pro/pkg/models"
+	"github.com/torkelo/grafana-pro/pkg/utils"
+)
+
+func GetAccount(c *middleware.Context) {
+	query := m.GetAccountInfoQuery{Id: c.UserAccount.Id}
+	err := bus.Dispatch(&query)
+
+	if err != nil {
+		c.JsonApiErr(500, "Failed to fetch collaboratos", err)
+		return
+	}
+
+	c.JSON(200, query.Result)
+}
+
+func AddCollaborator(c *middleware.Context) {
+	var cmd m.AddCollaboratorCommand
+
+	if !c.JsonBody(&cmd) {
+		c.JsonApiErr(400, "Invalid request", nil)
+		return
+	}
+
+	userQuery := m.GetAccountByLoginQuery{Login: cmd.Email}
+	err := bus.Dispatch(&userQuery)
+	if err != nil {
+		c.JsonApiErr(404, "Collaborator not found", nil)
+		return
+	}
+
+	accountToAdd := userQuery.Result
+
+	if accountToAdd.Id == c.UserAccount.Id {
+		c.JsonApiErr(400, "Cannot add yourself as collaborator", nil)
+		return
+	}
+
+	cmd.AccountId = accountToAdd.Id
+	cmd.ForAccountId = c.UserAccount.Id
+	cmd.Role = m.ROLE_READ_WRITE
+
+	err = bus.Dispatch(&cmd)
+	if err != nil {
+		c.JsonApiErr(500, "Could not add collaborator", err)
+		return
+	}
+
+	c.JsonOK("Collaborator added")
+}
+
+func GetOtherAccounts(c *middleware.Context) {
+	query := m.GetOtherAccountsQuery{AccountId: c.UserAccount.Id}
+	err := bus.Dispatch(&query)
+
+	if err != nil {
+		c.JSON(500, utils.DynMap{"message": err.Error()})
+		return
+	}
+
+	result := append(query.Result, &m.OtherAccountDTO{
+		Id:    c.UserAccount.Id,
+		Role:  "owner",
+		Email: c.UserAccount.Email,
+	})
+
+	for _, ac := range result {
+		if ac.Id == c.UserAccount.UsingAccountId {
+			ac.IsUsing = true
+			break
+		}
+	}
+
+	c.JSON(200, result)
+}
+
+func validateUsingAccount(accountId int64, otherId int64) bool {
+	if accountId == otherId {
+		return true
+	}
+
+	query := m.GetOtherAccountsQuery{AccountId: accountId}
+	err := bus.Dispatch(&query)
+	if err != nil {
+		return false
+	}
+
+	// validate that the account id in the list
+	valid := false
+	for _, other := range query.Result {
+		if other.Id == otherId {
+			valid = true
+		}
+	}
+	return valid
+}
+
+func SetUsingAccount(c *middleware.Context) {
+	usingAccountId := c.ParamsInt64(":id")
+
+	if !validateUsingAccount(c.UserAccount.Id, usingAccountId) {
+		c.JsonApiErr(401, "Not a valid account", nil)
+		return
+	}
+
+	cmd := m.SetUsingAccountCommand{
+		AccountId:      c.UserAccount.Id,
+		UsingAccountId: usingAccountId,
+	}
+
+	err := bus.Dispatch(&cmd)
+	if err != nil {
+		c.JsonApiErr(500, "Failed to update account", err)
+		return
+	}
+
+	c.JsonOK("Active account changed")
+}

+ 73 - 0
pkg/api/dashboard.go

@@ -0,0 +1,73 @@
+package api
+
+import (
+	"github.com/torkelo/grafana-pro/pkg/bus"
+	"github.com/torkelo/grafana-pro/pkg/middleware"
+	m "github.com/torkelo/grafana-pro/pkg/models"
+	"github.com/torkelo/grafana-pro/pkg/utils"
+)
+
+func GetDashboard(c *middleware.Context) {
+	slug := c.Params(":slug")
+
+	dash, err := m.GetDashboard(slug, c.GetAccountId())
+	if err != nil {
+		c.JsonApiErr(404, "Dashboard not found", nil)
+		return
+	}
+
+	dash.Data["id"] = dash.Id
+
+	c.JSON(200, dash.Data)
+}
+
+func DeleteDashboard(c *middleware.Context) {
+	slug := c.Params(":slug")
+
+	dash, err := m.GetDashboard(slug, c.GetAccountId())
+	if err != nil {
+		c.JsonApiErr(404, "Dashboard not found", nil)
+		return
+	}
+
+	err = m.DeleteDashboard(slug, c.GetAccountId())
+	if err != nil {
+		c.JsonApiErr(500, "Failed to delete dashboard", err)
+		return
+	}
+
+	var resp = map[string]interface{}{"title": dash.Title}
+
+	c.JSON(200, resp)
+}
+
+func Search(c *middleware.Context) {
+	query := c.Query("q")
+
+	results, err := m.SearchQuery(query, c.GetAccountId())
+	if err != nil {
+		c.JsonApiErr(500, "Search failed", err)
+		return
+	}
+
+	c.JSON(200, results)
+}
+
+func PostDashboard(c *middleware.Context) {
+	var cmd m.SaveDashboardCommand
+
+	if !c.JsonBody(&cmd) {
+		c.JsonApiErr(400, "bad request", nil)
+		return
+	}
+
+	cmd.AccountId = c.GetAccountId()
+
+	err := bus.Dispatch(&cmd)
+	if err != nil {
+		c.JsonApiErr(500, "Failed to save dashboard", err)
+		return
+	}
+
+	c.JSON(200, utils.DynMap{"status": "success", "slug": cmd.Result.Slug})
+}

+ 53 - 0
pkg/api/dataproxy.go

@@ -0,0 +1,53 @@
+package api
+
+import (
+	"net/http"
+	"net/http/httputil"
+	"net/url"
+
+	"github.com/torkelo/grafana-pro/pkg/bus"
+	"github.com/torkelo/grafana-pro/pkg/middleware"
+	m "github.com/torkelo/grafana-pro/pkg/models"
+	"github.com/torkelo/grafana-pro/pkg/utils"
+)
+
+func NewReverseProxy(ds *m.DataSource, proxyPath string) *httputil.ReverseProxy {
+	target, _ := url.Parse(ds.Url)
+
+	director := func(req *http.Request) {
+		req.URL.Scheme = target.Scheme
+		req.URL.Host = target.Host
+
+		reqQueryVals := req.URL.Query()
+
+		if ds.Type == m.DS_INFLUXDB {
+			req.URL.Path = utils.JoinUrlFragments(target.Path, "db/"+ds.Database+"/"+proxyPath)
+			reqQueryVals.Add("u", ds.User)
+			reqQueryVals.Add("p", ds.Password)
+			req.URL.RawQuery = reqQueryVals.Encode()
+		} else {
+			req.URL.Path = utils.JoinUrlFragments(target.Path, proxyPath)
+		}
+	}
+
+	return &httputil.ReverseProxy{Director: director}
+}
+
+// TODO: need to cache datasources
+func ProxyDataSourceRequest(c *middleware.Context) {
+	id := c.ParamsInt64(":id")
+
+	query := m.GetDataSourceByIdQuery{
+		Id:        id,
+		AccountId: c.GetAccountId(),
+	}
+
+	err := bus.Dispatch(&query)
+	if err != nil {
+		c.JsonApiErr(500, "Unable to load datasource meta data", err)
+	}
+
+	proxyPath := c.Params("*")
+	proxy := NewReverseProxy(&query.Result, proxyPath)
+	proxy.ServeHTTP(c.RW(), c.Req.Request)
+}

+ 58 - 0
pkg/api/dataproxy_test.go

@@ -0,0 +1,58 @@
+package api
+
+import (
+	"net/http"
+	"net/url"
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+
+	m "github.com/torkelo/grafana-pro/pkg/models"
+)
+
+func TestAccountDataAccess(t *testing.T) {
+
+	Convey("When getting graphite datasource proxy", t, func() {
+		ds := m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
+		proxy := NewReverseProxy(&ds, "/render")
+
+		requestUrl, _ := url.Parse("http://grafana.com/sub")
+		req := http.Request{URL: requestUrl}
+
+		proxy.Director(&req)
+
+		Convey("Can translate request url and path", func() {
+			So(req.URL.Host, ShouldEqual, "graphite:8080")
+			So(req.URL.Path, ShouldEqual, "/render")
+		})
+	})
+
+	Convey("When getting influxdb datasource proxy", t, func() {
+		ds := m.DataSource{
+			Type:     m.DS_INFLUXDB,
+			Url:      "http://influxdb:8083",
+			Database: "site",
+			User:     "user",
+			Password: "password",
+		}
+
+		proxy := NewReverseProxy(&ds, "")
+
+		requestUrl, _ := url.Parse("http://grafana.com/sub")
+		req := http.Request{URL: requestUrl}
+
+		proxy.Director(&req)
+
+		Convey("Should add db to url", func() {
+			So(req.URL.Path, ShouldEqual, "/db/site/")
+		})
+
+		Convey("Should add username and password", func() {
+			queryVals := req.URL.Query()
+			So(queryVals["u"][0], ShouldEqual, "user")
+			So(queryVals["p"][0], ShouldEqual, "password")
+		})
+
+	})
+
+}

+ 93 - 0
pkg/api/datasources.go

@@ -0,0 +1,93 @@
+package api
+
+import (
+	"github.com/torkelo/grafana-pro/pkg/api/dtos"
+	"github.com/torkelo/grafana-pro/pkg/bus"
+	"github.com/torkelo/grafana-pro/pkg/middleware"
+	m "github.com/torkelo/grafana-pro/pkg/models"
+)
+
+func GetDataSources(c *middleware.Context) {
+	query := m.GetDataSourcesQuery{AccountId: c.Account.Id}
+	err := bus.Dispatch(&query)
+
+	if err != nil {
+		c.JsonApiErr(500, "Failed to query datasources", err)
+		return
+	}
+
+	result := make([]*dtos.DataSource, len(query.Result))
+	for i, ds := range query.Result {
+		result[i] = &dtos.DataSource{
+			Id:        ds.Id,
+			AccountId: ds.AccountId,
+			Name:      ds.Name,
+			Url:       ds.Url,
+			Type:      ds.Type,
+			Access:    ds.Access,
+			Password:  ds.Password,
+			Database:  ds.Database,
+			User:      ds.User,
+			BasicAuth: ds.BasicAuth,
+		}
+	}
+
+	c.JSON(200, result)
+}
+
+func DeleteDataSource(c *middleware.Context) {
+	id := c.ParamsInt64(":id")
+
+	if id <= 0 {
+		c.JsonApiErr(400, "Missing valid datasource id", nil)
+		return
+	}
+
+	cmd := &m.DeleteDataSourceCommand{Id: id, AccountId: c.UserAccount.Id}
+
+	err := bus.Dispatch(cmd)
+	if err != nil {
+		c.JsonApiErr(500, "Failed to delete datasource", err)
+		return
+	}
+
+	c.JsonOK("Data source deleted")
+}
+
+func AddDataSource(c *middleware.Context) {
+	cmd := m.AddDataSourceCommand{}
+
+	if !c.JsonBody(&cmd) {
+		c.JsonApiErr(400, "Validation failed", nil)
+		return
+	}
+
+	cmd.AccountId = c.Account.Id
+
+	err := bus.Dispatch(&cmd)
+	if err != nil {
+		c.JsonApiErr(500, "Failed to add datasource", err)
+		return
+	}
+
+	c.JsonOK("Datasource added")
+}
+
+func UpdateDataSource(c *middleware.Context) {
+	cmd := m.UpdateDataSourceCommand{}
+
+	if !c.JsonBody(&cmd) {
+		c.JsonApiErr(400, "Validation failed", nil)
+		return
+	}
+
+	cmd.AccountId = c.Account.Id
+
+	err := bus.Dispatch(&cmd)
+	if err != nil {
+		c.JsonApiErr(500, "Failed to update datasource", err)
+		return
+	}
+
+	c.JsonOK("Datasource updated")
+}

+ 68 - 0
pkg/api/frontendsettings.go

@@ -0,0 +1,68 @@
+package api
+
+import (
+	"strconv"
+
+	"github.com/torkelo/grafana-pro/pkg/bus"
+	"github.com/torkelo/grafana-pro/pkg/middleware"
+	m "github.com/torkelo/grafana-pro/pkg/models"
+)
+
+func getFrontendSettings(c *middleware.Context) (map[string]interface{}, error) {
+	accountDataSources := make([]*m.DataSource, 0)
+
+	if c.Account != nil {
+		query := m.GetDataSourcesQuery{AccountId: c.Account.Id}
+		err := bus.Dispatch(&query)
+
+		if err != nil {
+			return nil, err
+		}
+
+		accountDataSources = query.Result
+	}
+
+	datasources := make(map[string]interface{})
+
+	for i, ds := range accountDataSources {
+		url := ds.Url
+
+		if ds.Access == m.DS_ACCESS_PROXY {
+			url = "/api/datasources/proxy/" + strconv.FormatInt(ds.Id, 10)
+		}
+
+		var dsMap = map[string]interface{}{
+			"type": ds.Type,
+			"url":  url,
+		}
+
+		if ds.Type == m.DS_INFLUXDB {
+			if ds.Access == m.DS_ACCESS_DIRECT {
+				dsMap["username"] = ds.User
+				dsMap["password"] = ds.Password
+				dsMap["url"] = url + "/db/" + ds.Database
+			}
+		}
+
+		// temp hack, first is always default
+		// TODO: implement default ds account setting
+		if i == 0 {
+			dsMap["default"] = true
+		}
+
+		datasources[ds.Name] = dsMap
+	}
+
+	// add grafana backend data source
+	datasources["grafana"] = map[string]interface{}{
+		"type":      "grafana",
+		"url":       "",
+		"grafanaDB": true,
+	}
+
+	jsonObj := map[string]interface{}{
+		"datasources": datasources,
+	}
+
+	return jsonObj, nil
+}

+ 61 - 0
pkg/api/login.go

@@ -0,0 +1,61 @@
+package api
+
+import (
+	"github.com/torkelo/grafana-pro/pkg/api/dtos"
+	"github.com/torkelo/grafana-pro/pkg/bus"
+	"github.com/torkelo/grafana-pro/pkg/log"
+	"github.com/torkelo/grafana-pro/pkg/middleware"
+	m "github.com/torkelo/grafana-pro/pkg/models"
+	"github.com/torkelo/grafana-pro/pkg/utils"
+)
+
+type loginJsonModel struct {
+	Email    string `json:"email" binding:"required"`
+	Password string `json:"password" binding:"required"`
+	Remember bool   `json:"remember"`
+}
+
+func LoginPost(c *middleware.Context) {
+	var loginModel loginJsonModel
+
+	if !c.JsonBody(&loginModel) {
+		c.JSON(400, utils.DynMap{"status": "bad request"})
+		return
+	}
+
+	userQuery := m.GetAccountByLoginQuery{Login: loginModel.Email}
+	err := bus.Dispatch(&userQuery)
+
+	if err != nil {
+		c.JsonApiErr(401, "Invalid username or password", err)
+		return
+	}
+
+	account := userQuery.Result
+
+	if loginModel.Password != account.Password {
+		c.JsonApiErr(401, "Invalid username or password", err)
+		return
+	}
+
+	loginUserWithAccount(account, c)
+
+	var resp = &dtos.LoginResult{}
+	resp.Status = "Logged in"
+	resp.User.Login = account.Login
+
+	c.JSON(200, resp)
+}
+
+func loginUserWithAccount(account *m.Account, c *middleware.Context) {
+	if account == nil {
+		log.Error(3, "Account login with nil account")
+	}
+
+	c.Session.Set("accountId", account.Id)
+}
+
+func LogoutPost(c *middleware.Context) {
+	c.Session.Delete("accountId")
+	c.JSON(200, utils.DynMap{"status": "logged out"})
+}

+ 78 - 0
pkg/api/login_oauth.go

@@ -0,0 +1,78 @@
+package api
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/torkelo/grafana-pro/pkg/bus"
+	"github.com/torkelo/grafana-pro/pkg/log"
+	"github.com/torkelo/grafana-pro/pkg/middleware"
+	m "github.com/torkelo/grafana-pro/pkg/models"
+	"github.com/torkelo/grafana-pro/pkg/setting"
+	"github.com/torkelo/grafana-pro/pkg/social"
+)
+
+func OAuthLogin(ctx *middleware.Context) {
+	if setting.OAuthService == nil {
+		ctx.Handle(404, "login.OAuthLogin(oauth service not enabled)", nil)
+		return
+	}
+
+	name := ctx.Params(":name")
+	connect, ok := social.SocialMap[name]
+	if !ok {
+		ctx.Handle(404, "login.OAuthLogin(social login not enabled)", errors.New(name))
+		return
+	}
+
+	code := ctx.Query("code")
+	if code == "" {
+		ctx.Redirect(connect.AuthCodeURL("", "online", "auto"))
+		return
+	}
+	log.Info("code: %v", code)
+
+	// handle call back
+	transport, err := connect.NewTransportFromCode(code)
+	if err != nil {
+		ctx.Handle(500, "login.OAuthLogin(NewTransportWithCode)", err)
+		return
+	}
+
+	log.Trace("login.OAuthLogin(Got token)")
+
+	userInfo, err := connect.UserInfo(transport)
+	if err != nil {
+		ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
+		return
+	}
+
+	log.Info("login.OAuthLogin(social login): %s", userInfo)
+
+	userQuery := m.GetAccountByLoginQuery{Login: userInfo.Email}
+	err = bus.Dispatch(&userQuery)
+
+	// create account if missing
+	if err == m.ErrAccountNotFound {
+		cmd := &m.CreateAccountCommand{
+			Login:   userInfo.Email,
+			Email:   userInfo.Email,
+			Name:    userInfo.Name,
+			Company: userInfo.Company,
+		}
+
+		if err = bus.Dispatch(&cmd); err != nil {
+			ctx.Handle(500, "Failed to create account", err)
+			return
+		}
+
+		userQuery.Result = &cmd.Result
+	} else if err != nil {
+		ctx.Handle(500, "Unexpected error", err)
+	}
+
+	// login
+	loginUserWithAccount(userQuery.Result, ctx)
+
+	ctx.Redirect("/")
+}

+ 26 - 0
pkg/api/register.go

@@ -0,0 +1,26 @@
+package api
+
+import (
+	"github.com/torkelo/grafana-pro/pkg/bus"
+	"github.com/torkelo/grafana-pro/pkg/middleware"
+	m "github.com/torkelo/grafana-pro/pkg/models"
+)
+
+func CreateAccount(c *middleware.Context) {
+	var cmd m.CreateAccountCommand
+
+	if !c.JsonBody(&cmd) {
+		c.JsonApiErr(400, "Validation error", nil)
+		return
+	}
+
+	cmd.Login = cmd.Email
+	err := bus.Dispatch(&cmd)
+
+	if err != nil {
+		c.JsonApiErr(500, "failed to create account", err)
+		return
+	}
+
+	c.JsonOK("Account created")
+}

+ 32 - 0
pkg/api/render.go

@@ -0,0 +1,32 @@
+package api
+
+import (
+	"net/http"
+	"strconv"
+
+	"github.com/torkelo/grafana-pro/pkg/components/renderer"
+	"github.com/torkelo/grafana-pro/pkg/middleware"
+	"github.com/torkelo/grafana-pro/pkg/utils"
+)
+
+func RenderToPng(c *middleware.Context) {
+	accountId := c.GetAccountId()
+	queryReader := utils.NewUrlQueryReader(c.Req.URL)
+	queryParams := "?render&accountId=" + strconv.FormatInt(accountId, 10) + "&" + c.Req.URL.RawQuery
+
+	renderOpts := &renderer.RenderOpts{
+		Url:    c.Params("*") + queryParams,
+		Width:  queryReader.Get("width", "800"),
+		Height: queryReader.Get("height", "400"),
+	}
+
+	renderOpts.Url = "http://localhost:3000/" + renderOpts.Url
+
+	pngPath, err := renderer.RenderToPng(renderOpts)
+	if err != nil {
+		c.HTML(500, "error.html", nil)
+	}
+
+	c.Resp.Header().Set("Content-Type", "image/png")
+	http.ServeFile(c.Resp, c.Req.Request, pngPath)
+}