Prechádzať zdrojové kódy

add Token authentication support

Added CRUD methods for Tokens.
Extend Auth Handler to check for the presence of a Bearer Authorization
header to authenticate against. If there is no header, or the token is not
valid, the Auth Handler falls back to looking for a Session.
woodsaj 11 rokov pred
rodič
commit
7b17e38f5d

+ 6 - 0
pkg/api/api.go

@@ -26,6 +26,12 @@ func Register(m *macaron.Macaron) {
 	m.Post("/api/account/using/:id", auth, SetUsingAccount)
 	m.Get("/api/account/others", auth, GetOtherAccounts)
 
+	// Token
+	m.Get("/api/tokens/list", auth, GetTokens)
+	m.Put("/api/tokens", auth, AddToken)
+	m.Post("/api/tokens", auth, UpdateToken)
+	m.Delete("/api/tokens/:id", auth, DeleteToken)
+
 	// data sources
 	m.Get("/acount/datasources/", auth, Index)
 	m.Get("/api/datasources/list", auth, GetDataSources)

+ 90 - 0
pkg/api/token.go

@@ -0,0 +1,90 @@
+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/util"
+)
+
+func GetTokens(c *middleware.Context) {
+	query := m.GetTokensQuery{AccountId: c.Account.Id}
+	err := bus.Dispatch(&query)
+
+	if err != nil {
+		c.JsonApiErr(500, "Failed to list tokens", err)
+		return
+	}
+	result := make([]*m.TokenDTO, len(query.Result))
+	for i, t := range query.Result {
+		result[i] = &m.TokenDTO{
+			Id:    t.Id,
+			Name:  t.Name,
+			Role:  t.Role,
+			Token: t.Token,
+		}
+	}
+	c.JSON(200, result)
+}
+
+func DeleteToken(c *middleware.Context) {
+	id := c.ParamsInt64(":id")
+
+	cmd := &m.DeleteTokenCommand{Id: id, AccountId: c.UserAccount.Id}
+
+	err := bus.Dispatch(cmd)
+	if err != nil {
+		c.JsonApiErr(500, "Failed to delete token", err)
+		return
+	}
+
+	c.JsonOK("Token deleted")
+}
+
+func AddToken(c *middleware.Context) {
+	cmd := m.AddTokenCommand{}
+
+	if !c.JsonBody(&cmd) {
+		c.JsonApiErr(400, "Validation failed", nil)
+		return
+	}
+
+	if cmd.Role != m.ROLE_READ_WRITE && cmd.Role != m.ROLE_READ {
+		c.JsonApiErr(400, "Invalid role specified", nil)
+		return
+	}
+
+	cmd.AccountId = c.Account.Id
+	cmd.Token = util.GetRandomString(64)
+
+	if err := bus.Dispatch(&cmd); err != nil {
+		c.JsonApiErr(500, "Failed to add token", err)
+		return
+	}
+	result := &m.TokenDTO{
+		Id:    cmd.Result.Id,
+		Name:  cmd.Result.Name,
+		Role:  cmd.Result.Role,
+		Token: cmd.Result.Token,
+	}
+	c.JSON(200, result)
+}
+
+func UpdateToken(c *middleware.Context) {
+	cmd := m.UpdateTokenCommand{}
+
+	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 token", err)
+		return
+	}
+
+	c.JsonOK("Token updated")
+}

+ 53 - 23
pkg/middleware/auth.go

@@ -2,10 +2,10 @@ package middleware
 
 import (
 	"errors"
-	"strconv"
-
 	"github.com/Unknwon/macaron"
 	"github.com/macaron-contrib/session"
+	"strconv"
+	"strings"
 
 	"github.com/torkelo/grafana-pro/pkg/bus"
 	m "github.com/torkelo/grafana-pro/pkg/models"
@@ -39,30 +39,60 @@ func authDenied(c *Context) {
 	c.Redirect(setting.AppSubUrl + "/login")
 }
 
-func Auth() macaron.Handler {
-	return func(c *Context, sess session.Store) {
-		accountId, err := authGetRequestAccountId(c, sess)
+func authByToken(c *Context) {
+	header := c.Req.Header.Get("Authorization")
+	parts := strings.SplitN(header, " ", 2)
+	if len(parts) != 2 || parts[0] != "Bearer" {
+		return
+	}
+	token := parts[1]
+	userQuery := m.GetAccountByTokenQuery{Token: token}
+	err := bus.Dispatch(&userQuery)
+	if err != nil {
+		return
+	}
 
-		if err != nil && c.Req.URL.Path != "/login" {
-			authDenied(c)
-			return
-		}
+	usingQuery := m.GetAccountByIdQuery{Id: userQuery.Result.UsingAccountId}
+	err = bus.Dispatch(&usingQuery)
+	if err != nil {
+		return
+	}
 
-		userQuery := m.GetAccountByIdQuery{Id: accountId}
-		err = bus.Dispatch(&userQuery)
-		if err != nil {
-			authDenied(c)
-			return
-		}
+	c.UserAccount = userQuery.Result
+	c.Account = usingQuery.Result
+}
 
-		usingQuery := m.GetAccountByIdQuery{Id: userQuery.Result.UsingAccountId}
-		err = bus.Dispatch(&usingQuery)
-		if err != nil {
-			authDenied(c)
-			return
-		}
+func authBySession(c *Context, sess session.Store) {
+	accountId, err := authGetRequestAccountId(c, sess)
 
-		c.UserAccount = userQuery.Result
-		c.Account = usingQuery.Result
+	if err != nil && c.Req.URL.Path != "/login" {
+		authDenied(c)
+		return
+	}
+
+	userQuery := m.GetAccountByIdQuery{Id: accountId}
+	err = bus.Dispatch(&userQuery)
+	if err != nil {
+		authDenied(c)
+		return
+	}
+
+	usingQuery := m.GetAccountByIdQuery{Id: userQuery.Result.UsingAccountId}
+	err = bus.Dispatch(&usingQuery)
+	if err != nil {
+		authDenied(c)
+		return
+	}
+
+	c.UserAccount = userQuery.Result
+	c.Account = usingQuery.Result
+}
+
+func Auth() macaron.Handler {
+	return func(c *Context, sess session.Store) {
+		authByToken(c)
+		if c.UserAccount == nil {
+			authBySession(c, sess)
+		}
 	}
 }

+ 9 - 11
pkg/models/account.go

@@ -23,23 +23,21 @@ type Account struct {
 	Company         string
 	NextDashboardId int
 	UsingAccountId  int64
-
-	Created time.Time
-	Updated time.Time
+	Created         time.Time
+	Updated         time.Time
 }
 
 // ---------------------
 // COMMANDS
 
 type CreateAccountCommand struct {
-	Email    string `json:"email" binding:"required"`
-	Login    string `json:"login"`
-	Password string `json:"password" binding:"required"`
-	Name     string `json:"name"`
-	Company  string `json:"company"`
-	Salt     string `json:"-"`
-
-	Result Account `json:"-"`
+	Email    string  `json:"email" binding:"required"`
+	Login    string  `json:"login"`
+	Password string  `json:"password" binding:"required"`
+	Name     string  `json:"name"`
+	Company  string  `json:"company"`
+	Salt     string  `json:"-"`
+	Result   Account `json:"-"`
 }
 
 type SetUsingAccountCommand struct {

+ 62 - 0
pkg/models/token.go

@@ -0,0 +1,62 @@
+package models
+
+import (
+	"time"
+)
+
+type Token struct {
+	Id        int64
+	AccountId int64    `xorm:"not null unique(uix_account_id_name)"`
+	Name      string   `xorm:"not null unique(uix_account_id_name)"`
+	Token     string   `xorm:"UNIQUE NOT NULL"`
+	Role      RoleType `xorm:"not null"`
+	Created   time.Time
+	Updated   time.Time
+}
+
+// ---------------------
+// COMMANDS
+type AddTokenCommand struct {
+	Name      string   `json:"name" binding:"required"`
+	Role      RoleType `json:"role" binding:"required"`
+	AccountId int64    `json:"-"`
+	Token     string   `json:"-"`
+	Result    *Token   `json:"-"`
+}
+
+type UpdateTokenCommand struct {
+	Id        int64    `json:"id"`
+	Name      string   `json:"name"`
+	AccountId int64    `json:"-"`
+	Role      RoleType `json:"role"`
+	Result    *Token   `json:"-"`
+}
+
+type DeleteTokenCommand struct {
+	Id        int64  `json:"id"`
+	AccountId int64  `json:"-"`
+	Result    *Token `json:"-"`
+}
+
+// ----------------------
+// QUERIES
+
+type GetTokensQuery struct {
+	AccountId int64
+	Result    []*Token
+}
+
+type GetAccountByTokenQuery struct {
+	Token  string
+	Result *Account
+}
+
+// ------------------------
+// DTO & Projections
+
+type TokenDTO struct {
+	Id    int64    `json:"id"`
+	Name  string   `json:"name"`
+	Token string   `json:"token"`
+	Role  RoleType `json:"role"`
+}

+ 22 - 0
pkg/stores/sqlstore/accounts.go

@@ -17,6 +17,7 @@ func init() {
 	bus.AddHandler("sql", SetUsingAccount)
 	bus.AddHandler("sql", GetAccountById)
 	bus.AddHandler("sql", GetAccountByLogin)
+	bus.AddHandler("sql", GetAccountByToken)
 }
 
 func CreateAccount(cmd *m.CreateAccountCommand) error {
@@ -109,6 +110,27 @@ func GetAccountById(query *m.GetAccountByIdQuery) error {
 	return nil
 }
 
+func GetAccountByToken(query *m.GetAccountByTokenQuery) error {
+	var err error
+
+	var account m.Account
+	has, err := x.Where("token=?", query.Token).Get(&account)
+
+	if err != nil {
+		return err
+	} else if has == false {
+		return m.ErrAccountNotFound
+	}
+
+	if account.UsingAccountId == 0 {
+		account.UsingAccountId = account.Id
+	}
+
+	query.Result = &account
+
+	return nil
+}
+
 func GetAccountByLogin(query *m.GetAccountByLoginQuery) error {
 	var err error
 

+ 2 - 1
pkg/stores/sqlstore/sqlstore.go

@@ -38,7 +38,8 @@ func init() {
 	tables = make([]interface{}, 0)
 
 	tables = append(tables, new(m.Account), new(m.Dashboard),
-		new(m.Collaborator), new(m.DataSource), new(DashboardTag))
+		new(m.Collaborator), new(m.DataSource), new(DashboardTag),
+		new(m.Token))
 }
 
 func Init() {

+ 66 - 0
pkg/stores/sqlstore/tokens.go

@@ -0,0 +1,66 @@
+package sqlstore
+
+import (
+	"github.com/go-xorm/xorm"
+	"github.com/torkelo/grafana-pro/pkg/bus"
+	m "github.com/torkelo/grafana-pro/pkg/models"
+	"time"
+)
+
+func init() {
+	bus.AddHandler("sql", GetTokens)
+	bus.AddHandler("sql", AddToken)
+	bus.AddHandler("sql", UpdateToken)
+	bus.AddHandler("sql", DeleteToken)
+}
+
+func GetTokens(query *m.GetTokensQuery) error {
+	sess := x.Limit(100, 0).Where("account_id=?", query.AccountId).Asc("name")
+
+	query.Result = make([]*m.Token, 0)
+	return sess.Find(&query.Result)
+}
+
+func DeleteToken(cmd *m.DeleteTokenCommand) error {
+	return inTransaction(func(sess *xorm.Session) error {
+		var rawSql = "DELETE FROM token WHERE id=? and account_id=?"
+		_, err := sess.Exec(rawSql, cmd.Id, cmd.AccountId)
+		return err
+	})
+}
+
+func AddToken(cmd *m.AddTokenCommand) error {
+
+	return inTransaction(func(sess *xorm.Session) error {
+		t := m.Token{
+			AccountId: cmd.AccountId,
+			Name:      cmd.Name,
+			Role:      cmd.Role,
+			Token:     cmd.Token,
+			Created:   time.Now(),
+			Updated:   time.Now(),
+		}
+
+		if _, err := sess.Insert(&t); err != nil {
+			return err
+		}
+		cmd.Result = &t
+		return nil
+	})
+}
+
+func UpdateToken(cmd *m.UpdateTokenCommand) error {
+
+	return inTransaction(func(sess *xorm.Session) error {
+		t := m.Token{
+			Id:        cmd.Id,
+			AccountId: cmd.AccountId,
+			Name:      cmd.Name,
+			Role:      cmd.Role,
+			Updated:   time.Now(),
+		}
+
+		_, err := sess.Where("id=? and account_id=?", t.Id, t.AccountId).Update(&t)
+		return err
+	})
+}