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

This commit is contained in:
Torkel Ödegaard 2015-02-26 17:23:28 +01:00
parent 6a2a6afc1d
commit c75aa23092
12 changed files with 170 additions and 70 deletions

View File

@ -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{ result := &dtos.NewApiKeyResult{
Id: cmd.Result.Id,
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
pkg/api/dtos/apikey.go Normal file
View File

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

View File

@ -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 HashedKey string
JsonKeyEncoded string ClientSecret string
} }
type ApiKeyJson struct { type ApiKeyJson struct {
Key string Key string `json:"k"`
AccountId int64 Name string `json:"n"`
Name string 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
} }

View File

@ -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)
})
})
}

View File

@ -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 != "" { } else if keyString := getApiKey(ctx); keyString != "" {
// Try API Key auth // base64 decode key
keyQuery := m.GetApiKeyByKeyQuery{Key: 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.OrgRole = apikey.Role
ctx.ApiKeyId = keyInfo.Id ctx.ApiKeyId = apikey.Id
ctx.OrgId = keyInfo.OrgId ctx.OrgId = apikey.OrgId
} }
} else if setting.AnonymousEnabled { } else if setting.AnonymousEnabled {
orgQuery := m.GetOrgByNameQuery{Name: setting.AnonymousOrgName} orgQuery := m.GetOrgByNameQuery{Name: setting.AnonymousOrgName}

View File

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

View File

@ -10,8 +10,7 @@ import (
func init() { func init() {
bus.AddHandler("sql", GetApiKeys) bus.AddHandler("sql", GetApiKeys)
bus.AddHandler("sql", GetApiKeyByKey) bus.AddHandler("sql", GetApiKeyByName)
bus.AddHandler("sql", UpdateApiKey)
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 { func GetApiKeyByName(query *m.GetApiKeyByNameQuery) 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 {
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

View File

@ -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() { Convey("Should be able to get key by name", func() {
query := m.GetApiKeyByKeyQuery{Key: "hello"} query := m.GetApiKeyByNameQuery{KeyName: "hello", OrgId: 1}
err = GetApiKeyByKey(&query) err = GetApiKeyByName(&query)
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(query.Result, ShouldNotBeNil) So(query.Result, ShouldNotBeNil)

View File

@ -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();

View File

@ -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>

View File

@ -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>

View File

@ -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>