Browse Source

New implementation for API Keys that only stores hashed api keys, and the client key is base64 decoded json web token with the unhashed key, Closes #1440

Torkel Ödegaard 10 years ago
parent
commit
c75aa23092

+ 7 - 22
pkg/api/apikey.go

@@ -1,10 +1,11 @@
 package api
 package api
 
 
 import (
 import (
+	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/components/apikeygen"
 	"github.com/grafana/grafana/pkg/middleware"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/util"
 )
 )
 
 
 func GetApiKeys(c *middleware.Context) {
 func GetApiKeys(c *middleware.Context) {
@@ -47,35 +48,19 @@ func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) {
 	}
 	}
 
 
 	cmd.OrgId = c.OrgId
 	cmd.OrgId = c.OrgId
-	cmd.Key = util.GetRandomString(64)
+
+	newKeyInfo := apikeygen.New(cmd.OrgId, cmd.Name)
+	cmd.Key = newKeyInfo.HashedKey
 
 
 	if err := bus.Dispatch(&cmd); err != nil {
 	if err := bus.Dispatch(&cmd); err != nil {
 		c.JsonApiErr(500, "Failed to add API key", err)
 		c.JsonApiErr(500, "Failed to add API key", err)
 		return
 		return
 	}
 	}
 
 
-	result := &m.ApiKeyDTO{
-		Id:   cmd.Result.Id,
+	result := &dtos.NewApiKeyResult{
 		Name: cmd.Result.Name,
 		Name: cmd.Result.Name,
-		Role: cmd.Result.Role,
+		Key:  newKeyInfo.ClientSecret,
 	}
 	}
 
 
 	c.JSON(200, result)
 	c.JSON(200, result)
 }
 }
-
-func UpdateApiKey(c *middleware.Context, cmd m.UpdateApiKeyCommand) {
-	if !cmd.Role.IsValid() {
-		c.JsonApiErr(400, "Invalid role specified", nil)
-		return
-	}
-
-	cmd.OrgId = c.OrgId
-
-	err := bus.Dispatch(&cmd)
-	if err != nil {
-		c.JsonApiErr(500, "Failed to update api key", err)
-		return
-	}
-
-	c.JsonOK("API key updated")
-}

+ 6 - 0
pkg/api/dtos/apikey.go

@@ -0,0 +1,6 @@
+package dtos
+
+type NewApiKeyResult struct {
+	Name string `json:"name"`
+	Key  string `json:"key"`
+}

+ 37 - 9
pkg/components/apikeygen/apikeygen.go

@@ -1,30 +1,58 @@
 package apikeygen
 package apikeygen
 
 
 import (
 import (
-	"strconv"
+	"encoding/base64"
+	"encoding/json"
+	"errors"
 
 
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/grafana/grafana/pkg/util"
 )
 )
 
 
+var ErrInvalidApiKey = errors.New("Invalid Api Key")
+
 type KeyGenResult struct {
 type KeyGenResult struct {
-	HashedKey      string
-	JsonKeyEncoded string
+	HashedKey    string
+	ClientSecret string
 }
 }
 
 
 type ApiKeyJson struct {
 type ApiKeyJson struct {
-	Key       string
-	AccountId int64
-	Name      string
+	Key   string `json:"k"`
+	Name  string `json:"n"`
+	OrgId int64  `json:"id"`
 }
 }
 
 
-func GenerateNewKey(accountId int64, name string) KeyGenResult {
+func New(orgId int64, name string) KeyGenResult {
 	jsonKey := ApiKeyJson{}
 	jsonKey := ApiKeyJson{}
 
 
-	jsonKey.AccountId = accountId
+	jsonKey.OrgId = orgId
 	jsonKey.Name = name
 	jsonKey.Name = name
 	jsonKey.Key = util.GetRandomString(32)
 	jsonKey.Key = util.GetRandomString(32)
 
 
 	result := KeyGenResult{}
 	result := KeyGenResult{}
-	result.HashedKey = util.EncodePassword([]byte(jsonKey.Key), []byte(strconv.FormatInt(accountId, 10)))
+	result.HashedKey = util.EncodePassword(jsonKey.Key, name)
+
+	jsonString, _ := json.Marshal(jsonKey)
+
+	result.ClientSecret = base64.StdEncoding.EncodeToString([]byte(jsonString))
+	return result
+}
+
+func Decode(keyString string) (*ApiKeyJson, error) {
+	jsonString, err := base64.StdEncoding.DecodeString(keyString)
+	if err != nil {
+		return nil, ErrInvalidApiKey
+	}
+
+	var keyObj ApiKeyJson
+	err = json.Unmarshal([]byte(jsonString), &keyObj)
+	if err != nil {
+		return nil, ErrInvalidApiKey
+	}
+
+	return &keyObj, nil
+}
 
 
+func IsValid(key *ApiKeyJson, hashedKey string) bool {
+	check := util.EncodePassword(key.Key, key.Name)
+	return check == hashedKey
 }
 }

+ 26 - 0
pkg/components/apikeygen/apikeygen_test.go

@@ -0,0 +1,26 @@
+package apikeygen
+
+import (
+	"testing"
+
+	"github.com/grafana/grafana/pkg/util"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestApiKeyGen(t *testing.T) {
+
+	Convey("When generating new api key", t, func() {
+		result := New(12, "Cool key")
+
+		So(result.ClientSecret, ShouldNotBeEmpty)
+		So(result.HashedKey, ShouldNotBeEmpty)
+
+		Convey("can decode key", func() {
+			keyInfo, err := Decode(result.ClientSecret)
+			So(err, ShouldBeNil)
+
+			keyHashed := util.EncodePassword(keyInfo.Key, keyInfo.Name)
+			So(keyHashed, ShouldEqual, result.HashedKey)
+		})
+	})
+}

+ 20 - 7
pkg/middleware/middleware.go

@@ -9,6 +9,7 @@ import (
 	"github.com/macaron-contrib/session"
 	"github.com/macaron-contrib/session"
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/components/apikeygen"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
@@ -43,22 +44,34 @@ func GetContextHandler() macaron.Handler {
 				ctx.SignedInUser = query.Result
 				ctx.SignedInUser = query.Result
 				ctx.IsSignedIn = true
 				ctx.IsSignedIn = true
 			}
 			}
-		} else if key := getApiKey(ctx); key != "" {
-			// Try API Key auth
-			keyQuery := m.GetApiKeyByKeyQuery{Key: key}
+		} else if keyString := getApiKey(ctx); keyString != "" {
+			// base64 decode key
+			decoded, err := apikeygen.Decode(keyString)
+			if err != nil {
+				ctx.JsonApiErr(401, "Invalid API key", err)
+				return
+			}
+			// fetch key
+			keyQuery := m.GetApiKeyByNameQuery{KeyName: decoded.Name, OrgId: decoded.OrgId}
 			if err := bus.Dispatch(&keyQuery); err != nil {
 			if err := bus.Dispatch(&keyQuery); err != nil {
 				ctx.JsonApiErr(401, "Invalid API key", err)
 				ctx.JsonApiErr(401, "Invalid API key", err)
 				return
 				return
 			} else {
 			} else {
-				keyInfo := keyQuery.Result
+				apikey := keyQuery.Result
+
+				// validate api key
+				if !apikeygen.IsValid(decoded, apikey.Key) {
+					ctx.JsonApiErr(401, "Invalid API key", err)
+					return
+				}
 
 
 				ctx.IsSignedIn = true
 				ctx.IsSignedIn = true
 				ctx.SignedInUser = &m.SignedInUser{}
 				ctx.SignedInUser = &m.SignedInUser{}
 
 
 				// TODO: fix this
 				// TODO: fix this
-				ctx.OrgRole = keyInfo.Role
-				ctx.ApiKeyId = keyInfo.Id
-				ctx.OrgId = keyInfo.OrgId
+				ctx.OrgRole = apikey.Role
+				ctx.ApiKeyId = apikey.Id
+				ctx.OrgId = apikey.OrgId
 			}
 			}
 		} else if setting.AnonymousEnabled {
 		} else if setting.AnonymousEnabled {
 			orgQuery := m.GetOrgByNameQuery{Name: setting.AnonymousOrgName}
 			orgQuery := m.GetOrgByNameQuery{Name: setting.AnonymousOrgName}

+ 4 - 3
pkg/models/apikey.go

@@ -49,9 +49,10 @@ type GetApiKeysQuery struct {
 	Result []*ApiKey
 	Result []*ApiKey
 }
 }
 
 
-type GetApiKeyByKeyQuery struct {
-	Key    string
-	Result *ApiKey
+type GetApiKeyByNameQuery struct {
+	KeyName string
+	OrgId   int64
+	Result  *ApiKey
 }
 }
 
 
 // ------------------------
 // ------------------------

+ 3 - 18
pkg/services/sqlstore/apikey.go

@@ -10,8 +10,7 @@ import (
 
 
 func init() {
 func init() {
 	bus.AddHandler("sql", GetApiKeys)
 	bus.AddHandler("sql", GetApiKeys)
-	bus.AddHandler("sql", GetApiKeyByKey)
-	bus.AddHandler("sql", UpdateApiKey)
+	bus.AddHandler("sql", GetApiKeyByName)
 	bus.AddHandler("sql", DeleteApiKey)
 	bus.AddHandler("sql", DeleteApiKey)
 	bus.AddHandler("sql", AddApiKey)
 	bus.AddHandler("sql", AddApiKey)
 }
 }
@@ -50,23 +49,9 @@ func AddApiKey(cmd *m.AddApiKeyCommand) error {
 	})
 	})
 }
 }
 
 
-func UpdateApiKey(cmd *m.UpdateApiKeyCommand) error {
-	return inTransaction(func(sess *xorm.Session) error {
-		t := m.ApiKey{
-			Id:      cmd.Id,
-			OrgId:   cmd.OrgId,
-			Name:    cmd.Name,
-			Role:    cmd.Role,
-			Updated: time.Now(),
-		}
-		_, err := sess.Where("id=? and org_id=?", t.Id, t.OrgId).Update(&t)
-		return err
-	})
-}
-
-func GetApiKeyByKey(query *m.GetApiKeyByKeyQuery) error {
+func GetApiKeyByName(query *m.GetApiKeyByNameQuery) error {
 	var apikey m.ApiKey
 	var apikey m.ApiKey
-	has, err := x.Where("`key`=?", query.Key).Get(&apikey)
+	has, err := x.Where("org_id=? AND name=?", query.OrgId, query.KeyName).Get(&apikey)
 
 
 	if err != nil {
 	if err != nil {
 		return err
 		return err

+ 4 - 4
pkg/services/sqlstore/apikey_test.go

@@ -14,13 +14,13 @@ func TestApiKeyDataAccess(t *testing.T) {
 		InitTestDB(t)
 		InitTestDB(t)
 
 
 		Convey("Given saved api key", func() {
 		Convey("Given saved api key", func() {
-			cmd := m.AddApiKeyCommand{OrgId: 1, Key: "hello"}
+			cmd := m.AddApiKeyCommand{OrgId: 1, Name: "hello", Key: "asd"}
 			err := AddApiKey(&cmd)
 			err := AddApiKey(&cmd)
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
-			Convey("Should be able to get key by key", func() {
-				query := m.GetApiKeyByKeyQuery{Key: "hello"}
-				err = GetApiKeyByKey(&query)
+			Convey("Should be able to get key by name", func() {
+				query := m.GetApiKeyByNameQuery{KeyName: "hello", OrgId: 1}
+				err = GetApiKeyByName(&query)
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
 
 
 				So(query.Result, ShouldNotBeNil)
 				So(query.Result, ShouldNotBeNil)

+ 10 - 1
src/app/features/org/orgApiKeysCtrl.js

@@ -26,7 +26,16 @@ function (angular) {
     };
     };
 
 
     $scope.addToken = function() {
     $scope.addToken = function() {
-      backendSrv.post('/api/auth/keys', $scope.token).then($scope.getTokens);
+      backendSrv.post('/api/auth/keys', $scope.token).then(function(result) {
+
+        var modalScope = $scope.$new(true);
+        modalScope.key = result.key;
+
+        $scope.appEvent('show-modal', {
+          src: './app/features/org/partials/apikeyModal.html',
+          scope: modalScope
+        });
+      });
     };
     };
 
 
     $scope.init();
     $scope.init();

+ 44 - 0
src/app/features/org/partials/apikeyModal.html

@@ -0,0 +1,44 @@
+<div class="modal-body gf-box gf-box-no-margin">
+	<div class="gf-box-header">
+		<div class="gf-box-title">
+			<i class="fa fa-key"></i>
+			API Key Created
+		</div>
+
+		<button class="gf-box-header-close-btn" ng-click="dismiss();">
+			<i class="fa fa-remove"></i>
+		</button>
+	</div>
+
+	<div class="gf-box-body" style="min-height: 0px;">
+
+		<div class="tight-form last">
+			<ul class="tight-form-list">
+				<li class="tight-form-item">
+					<strong>Key</strong>
+				</li>
+				<li class="tight-form-item last">
+					{{key}}
+				</li>
+			</ul>
+			<div class="clearfix"></div>
+		</div>
+		<br>
+		<br>
+
+		<div class="grafana-info-box" style="text-align: left">
+			You will only be able to view this key here once! It is not stored in this form. So be sure to copy it now.
+			<br>
+			<br>
+			You can authenticate request using the Authorization HTTP header, example:
+			<br>
+			<br>
+			<pre class="small" style="overflow: hidden">
+curl -H "Authorization: Bearer your_key_above" http://your.grafana.com/api/dashboards/db/mydash
+			</pre>
+		</div>
+
+	</div>
+
+</div>
+

+ 6 - 3
src/app/features/org/partials/orgApiKeys.html

@@ -31,13 +31,16 @@
 				<div class="clearfix"></div>
 				<div class="clearfix"></div>
 			</ul>
 			</ul>
 		</form>
 		</form>
-		<br>
 
 
-		<table class="grafana-options-table">
+		<table class="grafana-options-table" style="width: 250px">
+			<tr>
+				<th style="text-align: left">Name</th>
+				<th style="text-align: left">Role</th>
+				<th></th>
+			</tr>
 			<tr ng-repeat="t in tokens">
 			<tr ng-repeat="t in tokens">
 				<td>{{t.name}}</td>
 				<td>{{t.name}}</td>
 				<td>{{t.role}}</td>
 				<td>{{t.role}}</td>
-				<td>{{t.key}}</td>
 				<td style="width: 1%">
 				<td style="width: 1%">
 					<a ng-click="removeToken(t.id)" class="btn btn-danger btn-mini">
 					<a ng-click="removeToken(t.id)" class="btn btn-danger btn-mini">
 						<i class="fa fa-remove"></i>
 						<i class="fa fa-remove"></i>

+ 3 - 3
src/app/features/org/partials/orgDetails.html

@@ -28,7 +28,7 @@
 							<strong>Address 1</strong>
 							<strong>Address 1</strong>
 						</li>
 						</li>
 						<li>
 						<li>
-							<input type="text" required ng-model="org.address1" class="input-xxlarge tight-form-input last" >
+							<input type="text" ng-model="org.address1" class="input-xxlarge tight-form-input last" >
 						</li>
 						</li>
 					</ul>
 					</ul>
 					<div class="clearfix"></div>
 					<div class="clearfix"></div>
@@ -39,7 +39,7 @@
 							<strong>Address 2</strong>
 							<strong>Address 2</strong>
 						</li>
 						</li>
 						<li>
 						<li>
-							<input type="text" required ng-model="org.address2" class="input-xxlarge tight-form-input last" >
+							<input type="text" ng-model="org.address2" class="input-xxlarge tight-form-input last" >
 						</li>
 						</li>
 					</ul>
 					</ul>
 					<div class="clearfix"></div>
 					<div class="clearfix"></div>
@@ -50,7 +50,7 @@
 							<strong>City</strong>
 							<strong>City</strong>
 						</li>
 						</li>
 						<li>
 						<li>
-							<input type="text" required ng-model="org.city" class="input-xxlarge tight-form-input last" >
+							<input type="text" ng-model="org.city" class="input-xxlarge tight-form-input last" >
 						</li>
 						</li>
 					</ul>
 					</ul>
 					<div class="clearfix"></div>
 					<div class="clearfix"></div>