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
import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/apikeygen"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
)
func GetApiKeys(c *middleware.Context) {
@ -47,35 +48,19 @@ func AddApiKey(c *middleware.Context, cmd m.AddApiKeyCommand) {
}
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 {
c.JsonApiErr(500, "Failed to add API key", err)
return
}
result := &m.ApiKeyDTO{
Id: cmd.Result.Id,
result := &dtos.NewApiKeyResult{
Name: cmd.Result.Name,
Role: cmd.Result.Role,
Key: newKeyInfo.ClientSecret,
}
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
import (
"strconv"
"encoding/base64"
"encoding/json"
"errors"
"github.com/grafana/grafana/pkg/util"
)
var ErrInvalidApiKey = errors.New("Invalid Api Key")
type KeyGenResult struct {
HashedKey string
JsonKeyEncoded string
HashedKey string
ClientSecret string
}
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.AccountId = accountId
jsonKey.OrgId = orgId
jsonKey.Name = name
jsonKey.Key = util.GetRandomString(32)
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/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/apikeygen"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/setting"
@ -43,22 +44,34 @@ func GetContextHandler() macaron.Handler {
ctx.SignedInUser = query.Result
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 {
ctx.JsonApiErr(401, "Invalid API key", err)
return
} 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.SignedInUser = &m.SignedInUser{}
// 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 {
orgQuery := m.GetOrgByNameQuery{Name: setting.AnonymousOrgName}

View File

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

View File

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

View File

@ -14,13 +14,13 @@ func TestApiKeyDataAccess(t *testing.T) {
InitTestDB(t)
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)
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(query.Result, ShouldNotBeNil)

View File

@ -26,7 +26,16 @@ function (angular) {
};
$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();

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>
</ul>
</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">
<td>{{t.name}}</td>
<td>{{t.role}}</td>
<td>{{t.key}}</td>
<td style="width: 1%">
<a ng-click="removeToken(t.id)" class="btn btn-danger btn-mini">
<i class="fa fa-remove"></i>

View File

@ -28,7 +28,7 @@
<strong>Address 1</strong>
</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>
</ul>
<div class="clearfix"></div>
@ -39,7 +39,7 @@
<strong>Address 2</strong>
</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>
</ul>
<div class="clearfix"></div>
@ -50,7 +50,7 @@
<strong>City</strong>
</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>
</ul>
<div class="clearfix"></div>