mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
6a2a6afc1d
commit
c75aa23092
@ -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
6
pkg/api/dtos/apikey.go
Normal file
@ -0,0 +1,6 @@
|
||||
package dtos
|
||||
|
||||
type NewApiKeyResult struct {
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
}
|
@ -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
|
||||
}
|
||||
|
26
pkg/components/apikeygen/apikeygen_test.go
Normal file
26
pkg/components/apikeygen/apikeygen_test.go
Normal 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)
|
||||
})
|
||||
})
|
||||
}
|
@ -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();
|
||||
|
44
src/app/features/org/partials/apikeyModal.html
Normal file
44
src/app/features/org/partials/apikeyModal.html
Normal 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>
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user