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
|
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
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
|
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
|
||||||
}
|
}
|
||||||
|
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/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}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------
|
// ------------------------
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
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>
|
<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>
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user