Commit c75aa230 by Torkel Ödegaard

New implementation for API Keys that only stores hashed api keys, and the client…

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
parent 6a2a6afc
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")
}
package dtos
type NewApiKeyResult struct {
Name string `json:"name"`
Key string `json:"key"`
}
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
}
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)
})
})
}
......@@ -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}
......
......@@ -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
}
// ------------------------
......
......@@ -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
......
......@@ -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)
......
......@@ -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();
......
<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>
......@@ -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>
......
......@@ -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>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment