mirror of
https://github.com/grafana/grafana.git
synced 2024-12-28 18:01:40 -06:00
Chore: move from xorm to sqlx apikey store (#53434)
* migrate from xorm to sqlx * fix tests * fix comments * fix some comments on the PR * fix CI * fix the comments
This commit is contained in:
parent
45b65cc6c9
commit
ebcdf402b2
@ -306,7 +306,10 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool, prefs *
|
||||
}
|
||||
|
||||
hideApiKeys, _, _ := hs.kvStore.Get(c.Req.Context(), c.OrgID, "serviceaccounts", "hideApiKeys")
|
||||
apiKeys := hs.apiKeyService.GetAllAPIKeys(c.Req.Context(), c.OrgID)
|
||||
apiKeys, err := hs.apiKeyService.GetAllAPIKeys(c.Req.Context(), c.OrgID)
|
||||
if err != nil {
|
||||
return navTree, err
|
||||
}
|
||||
apiKeysHidden := hideApiKeys == "1" && len(apiKeys) == 0
|
||||
if hasAccess(ac.ReqOrgAdmin, apiKeyAccessEvaluator) && !apiKeysHidden {
|
||||
configNodes = append(configNodes, &dtos.NavLink{
|
||||
|
@ -6,7 +6,7 @@ import (
|
||||
|
||||
type Service interface {
|
||||
GetAPIKeys(ctx context.Context, query *GetApiKeysQuery) error
|
||||
GetAllAPIKeys(ctx context.Context, orgID int64) []*APIKey
|
||||
GetAllAPIKeys(ctx context.Context, orgID int64) ([]*APIKey, error)
|
||||
DeleteApiKey(ctx context.Context, cmd *DeleteCommand) error
|
||||
AddAPIKey(ctx context.Context, cmd *AddCommand) error
|
||||
GetApiKeyById(ctx context.Context, query *GetByIDQuery) error
|
||||
|
@ -13,13 +13,21 @@ type Service struct {
|
||||
}
|
||||
|
||||
func ProvideService(db db.DB, cfg *setting.Cfg) apikey.Service {
|
||||
if cfg.IsFeatureToggleEnabled("newDBLibrary") {
|
||||
return &Service{
|
||||
store: &sqlxStore{
|
||||
sess: db.GetSqlxSession(),
|
||||
cfg: cfg,
|
||||
},
|
||||
}
|
||||
}
|
||||
return &Service{store: &sqlStore{db: db, cfg: cfg}}
|
||||
}
|
||||
|
||||
func (s *Service) GetAPIKeys(ctx context.Context, query *apikey.GetApiKeysQuery) error {
|
||||
return s.store.GetAPIKeys(ctx, query)
|
||||
}
|
||||
func (s *Service) GetAllAPIKeys(ctx context.Context, orgID int64) []*apikey.APIKey {
|
||||
func (s *Service) GetAllAPIKeys(ctx context.Context, orgID int64) ([]*apikey.APIKey, error) {
|
||||
return s.store.GetAllAPIKeys(ctx, orgID)
|
||||
}
|
||||
func (s *Service) GetApiKeyById(ctx context.Context, query *apikey.GetByIDQuery) error {
|
||||
|
144
pkg/services/apikey/apikeyimpl/sqlx_store.go
Normal file
144
pkg/services/apikey/apikeyimpl/sqlx_store.go
Normal file
@ -0,0 +1,144 @@
|
||||
package apikeyimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/apikey"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/session"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type sqlxStore struct {
|
||||
sess *session.SessionDB
|
||||
cfg *setting.Cfg
|
||||
}
|
||||
|
||||
func (ss *sqlxStore) GetAPIKeys(ctx context.Context, query *apikey.GetApiKeysQuery) error {
|
||||
var where []string
|
||||
var args []interface{}
|
||||
|
||||
if query.IncludeExpired {
|
||||
where = append(where, "org_id=?")
|
||||
args = append(args, query.OrgId)
|
||||
} else {
|
||||
where = append(where, "org_id=? and ( expires IS NULL or expires >= ?)")
|
||||
args = append(args, query.OrgId, timeNow().Unix())
|
||||
}
|
||||
|
||||
where = append(where, "service_account_id IS NULL")
|
||||
|
||||
if !accesscontrol.IsDisabled(ss.cfg) {
|
||||
filter, err := accesscontrol.Filter(query.User, "id", "apikeys:id:", accesscontrol.ActionAPIKeyRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
where = append(where, filter.Where)
|
||||
args = append(args, filter.Args...)
|
||||
}
|
||||
|
||||
ws := fmt.Sprint(strings.Join(where[:], " AND "))
|
||||
qr := fmt.Sprintf(`SELECT * FROM api_key WHERE %s ORDER BY name ASC LIMIT 100`, ws)
|
||||
query.Result = make([]*apikey.APIKey, 0)
|
||||
err := ss.sess.Select(ctx, &query.Result, qr, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (ss *sqlxStore) GetAllAPIKeys(ctx context.Context, orgID int64) ([]*apikey.APIKey, error) {
|
||||
result := make([]*apikey.APIKey, 0)
|
||||
var err error
|
||||
if orgID != -1 {
|
||||
err = ss.sess.Select(
|
||||
ctx, &result, "SELECT * FROM api_key WHERE service_account_id IS NULL AND org_id = ? ORDER BY name ASC", orgID)
|
||||
} else {
|
||||
err = ss.sess.Select(
|
||||
ctx, &result, "SELECT * FROM api_key WHERE service_account_id IS NULL ORDER BY name ASC")
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (ss *sqlxStore) DeleteApiKey(ctx context.Context, cmd *apikey.DeleteCommand) error {
|
||||
res, err := ss.sess.Exec(ctx, "DELETE FROM api_key WHERE id=? and org_id=? and service_account_id IS NULL", cmd.Id, cmd.OrgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := res.RowsAffected()
|
||||
if err == nil && n == 0 {
|
||||
return apikey.ErrNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (ss *sqlxStore) AddAPIKey(ctx context.Context, cmd *apikey.AddCommand) error {
|
||||
updated := timeNow()
|
||||
var expires *int64 = nil
|
||||
if cmd.SecondsToLive > 0 {
|
||||
v := updated.Add(time.Second * time.Duration(cmd.SecondsToLive)).Unix()
|
||||
expires = &v
|
||||
} else if cmd.SecondsToLive < 0 {
|
||||
return apikey.ErrInvalidExpiration
|
||||
}
|
||||
|
||||
err := ss.GetApiKeyByName(ctx, &apikey.GetByNameQuery{OrgId: cmd.OrgId, KeyName: cmd.Name})
|
||||
// If key with the same orgId and name already exist return err
|
||||
if !errors.Is(err, apikey.ErrInvalid) {
|
||||
return apikey.ErrDuplicate
|
||||
}
|
||||
isRevoked := false
|
||||
t := apikey.APIKey{
|
||||
OrgId: cmd.OrgId,
|
||||
Name: cmd.Name,
|
||||
Role: cmd.Role,
|
||||
Key: cmd.Key,
|
||||
Created: updated,
|
||||
Updated: updated,
|
||||
Expires: expires,
|
||||
ServiceAccountId: nil,
|
||||
IsRevoked: &isRevoked,
|
||||
}
|
||||
|
||||
t.Id, err = ss.sess.ExecWithReturningId(ctx,
|
||||
`INSERT INTO api_key (org_id, name, role, "key", created, updated, expires, service_account_id, is_revoked) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, t.OrgId, t.Name, t.Role, t.Key, t.Created, t.Updated, t.Expires, t.ServiceAccountId, t.IsRevoked)
|
||||
cmd.Result = &t
|
||||
return err
|
||||
}
|
||||
|
||||
func (ss *sqlxStore) GetApiKeyById(ctx context.Context, query *apikey.GetByIDQuery) error {
|
||||
var key apikey.APIKey
|
||||
err := ss.sess.Get(ctx, &key, "SELECT * FROM api_key WHERE id=?", query.ApiKeyId)
|
||||
if err != nil && errors.Is(err, sql.ErrNoRows) {
|
||||
return apikey.ErrInvalid
|
||||
}
|
||||
query.Result = &key
|
||||
return err
|
||||
}
|
||||
|
||||
func (ss *sqlxStore) GetApiKeyByName(ctx context.Context, query *apikey.GetByNameQuery) error {
|
||||
var key apikey.APIKey
|
||||
err := ss.sess.Get(ctx, &key, "SELECT * FROM api_key WHERE org_id=? AND name=?", query.OrgId, query.KeyName)
|
||||
if err != nil && errors.Is(err, sql.ErrNoRows) {
|
||||
return apikey.ErrInvalid
|
||||
}
|
||||
query.Result = &key
|
||||
return err
|
||||
}
|
||||
|
||||
func (ss *sqlxStore) GetAPIKeyByHash(ctx context.Context, hash string) (*apikey.APIKey, error) {
|
||||
var key apikey.APIKey
|
||||
err := ss.sess.Get(ctx, &key, `SELECT * FROM api_key WHERE "key"=?`, hash)
|
||||
if err != nil && errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, apikey.ErrInvalid
|
||||
}
|
||||
return &key, err
|
||||
}
|
||||
|
||||
func (ss *sqlxStore) UpdateAPIKeyLastUsedDate(ctx context.Context, tokenID int64) error {
|
||||
now := timeNow()
|
||||
_, err := ss.sess.Exec(ctx, `UPDATE api_key SET last_used_at=? WHERE id=?`, &now, tokenID)
|
||||
return err
|
||||
}
|
13
pkg/services/apikey/apikeyimpl/sqlx_store_test.go
Normal file
13
pkg/services/apikey/apikeyimpl/sqlx_store_test.go
Normal file
@ -0,0 +1,13 @@
|
||||
package apikeyimpl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
)
|
||||
|
||||
func TestIntegrationSQLxApiKeyDataAccess(t *testing.T) {
|
||||
testIntegrationApiKeyDataAccess(t, func(ss *sqlstore.SQLStore) store {
|
||||
return &sqlxStore{sess: ss.GetSqlxSession(), cfg: ss.Cfg}
|
||||
})
|
||||
}
|
@ -2,21 +2,13 @@ package apikeyimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/apikey"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/db"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/pkg/errors"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type store interface {
|
||||
GetAPIKeys(ctx context.Context, query *apikey.GetApiKeysQuery) error
|
||||
GetAllAPIKeys(ctx context.Context, orgID int64) []*apikey.APIKey
|
||||
GetAllAPIKeys(ctx context.Context, orgID int64) ([]*apikey.APIKey, error)
|
||||
DeleteApiKey(ctx context.Context, cmd *apikey.DeleteCommand) error
|
||||
AddAPIKey(ctx context.Context, cmd *apikey.AddCommand) error
|
||||
GetApiKeyById(ctx context.Context, query *apikey.GetByIDQuery) error
|
||||
@ -24,169 +16,3 @@ type store interface {
|
||||
GetAPIKeyByHash(ctx context.Context, hash string) (*apikey.APIKey, error)
|
||||
UpdateAPIKeyLastUsedDate(ctx context.Context, tokenID int64) error
|
||||
}
|
||||
|
||||
type sqlStore struct {
|
||||
db db.DB
|
||||
cfg *setting.Cfg
|
||||
}
|
||||
|
||||
// timeNow makes it possible to test usage of time
|
||||
var timeNow = time.Now
|
||||
|
||||
func (ss *sqlStore) GetAPIKeys(ctx context.Context, query *apikey.GetApiKeysQuery) error {
|
||||
return ss.db.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
|
||||
var sess *xorm.Session
|
||||
|
||||
if query.IncludeExpired {
|
||||
sess = dbSession.Limit(100, 0).
|
||||
Where("org_id=?", query.OrgId).
|
||||
Asc("name")
|
||||
} else {
|
||||
sess = dbSession.Limit(100, 0).
|
||||
Where("org_id=? and ( expires IS NULL or expires >= ?)", query.OrgId, timeNow().Unix()).
|
||||
Asc("name")
|
||||
}
|
||||
|
||||
sess = sess.Where("service_account_id IS NULL")
|
||||
|
||||
if !accesscontrol.IsDisabled(ss.cfg) {
|
||||
filter, err := accesscontrol.Filter(query.User, "id", "apikeys:id:", accesscontrol.ActionAPIKeyRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sess.And(filter.Where, filter.Args...)
|
||||
}
|
||||
|
||||
query.Result = make([]*apikey.APIKey, 0)
|
||||
return sess.Find(&query.Result)
|
||||
})
|
||||
}
|
||||
|
||||
func (ss *sqlStore) GetAllAPIKeys(ctx context.Context, orgID int64) []*apikey.APIKey {
|
||||
result := make([]*apikey.APIKey, 0)
|
||||
err := ss.db.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
|
||||
sess := dbSession.Where("service_account_id IS NULL").Asc("name")
|
||||
if orgID != -1 {
|
||||
sess = sess.Where("org_id=?", orgID)
|
||||
}
|
||||
return sess.Find(&result)
|
||||
})
|
||||
if err != nil {
|
||||
_ = err
|
||||
// TODO: return error
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (ss *sqlStore) DeleteApiKey(ctx context.Context, cmd *apikey.DeleteCommand) error {
|
||||
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
rawSQL := "DELETE FROM api_key WHERE id=? and org_id=? and service_account_id IS NULL"
|
||||
result, err := sess.Exec(rawSQL, cmd.Id, cmd.OrgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
} else if n == 0 {
|
||||
return apikey.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (ss *sqlStore) AddAPIKey(ctx context.Context, cmd *apikey.AddCommand) error {
|
||||
return ss.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
key := apikey.APIKey{OrgId: cmd.OrgId, Name: cmd.Name}
|
||||
exists, _ := sess.Get(&key)
|
||||
if exists {
|
||||
return apikey.ErrDuplicate
|
||||
}
|
||||
|
||||
updated := timeNow()
|
||||
var expires *int64 = nil
|
||||
if cmd.SecondsToLive > 0 {
|
||||
v := updated.Add(time.Second * time.Duration(cmd.SecondsToLive)).Unix()
|
||||
expires = &v
|
||||
} else if cmd.SecondsToLive < 0 {
|
||||
return apikey.ErrInvalidExpiration
|
||||
}
|
||||
|
||||
isRevoked := false
|
||||
t := apikey.APIKey{
|
||||
OrgId: cmd.OrgId,
|
||||
Name: cmd.Name,
|
||||
Role: cmd.Role,
|
||||
Key: cmd.Key,
|
||||
Created: updated,
|
||||
Updated: updated,
|
||||
Expires: expires,
|
||||
ServiceAccountId: cmd.ServiceAccountID,
|
||||
IsRevoked: &isRevoked,
|
||||
}
|
||||
|
||||
if _, err := sess.Insert(&t); err != nil {
|
||||
return errors.Wrap(err, "failed to insert token")
|
||||
}
|
||||
|
||||
cmd.Result = &t
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (ss *sqlStore) GetApiKeyById(ctx context.Context, query *apikey.GetByIDQuery) error {
|
||||
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
var key apikey.APIKey
|
||||
has, err := sess.ID(query.ApiKeyId).Get(&key)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return apikey.ErrInvalid
|
||||
}
|
||||
|
||||
query.Result = &key
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (ss *sqlStore) GetApiKeyByName(ctx context.Context, query *apikey.GetByNameQuery) error {
|
||||
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
var key apikey.APIKey
|
||||
has, err := sess.Where("org_id=? AND name=?", query.OrgId, query.KeyName).Get(&key)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return apikey.ErrInvalid
|
||||
}
|
||||
|
||||
query.Result = &key
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (ss *sqlStore) GetAPIKeyByHash(ctx context.Context, hash string) (*apikey.APIKey, error) {
|
||||
var key apikey.APIKey
|
||||
err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
has, err := sess.Table("api_key").Where(fmt.Sprintf("%s = ?", ss.db.GetDialect().Quote("key")), hash).Get(&key)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return apikey.ErrInvalid
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return &key, err
|
||||
}
|
||||
|
||||
func (ss *sqlStore) UpdateAPIKeyLastUsedDate(ctx context.Context, tokenID int64) error {
|
||||
now := timeNow()
|
||||
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
if _, err := sess.Table("api_key").ID(tokenID).Cols("last_used_at").Update(&apikey.APIKey{LastUsedAt: &now}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
@ -15,6 +15,15 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
)
|
||||
|
||||
type getStore func(*sqlstore.SQLStore) store
|
||||
|
||||
type getApiKeysTestCase struct {
|
||||
desc string
|
||||
user *user.SignedInUser
|
||||
expectedNumKeys int
|
||||
expectedAllNumKeys int
|
||||
}
|
||||
|
||||
func mockTimeNow() {
|
||||
var timeSeed int64
|
||||
timeNow = func() time.Time {
|
||||
@ -29,7 +38,20 @@ func resetTimeNow() {
|
||||
timeNow = time.Now
|
||||
}
|
||||
|
||||
func TestIntegrationApiKeyDataAccess(t *testing.T) {
|
||||
func seedApiKeys(t *testing.T, store store, num int) {
|
||||
t.Helper()
|
||||
|
||||
for i := 0; i < num; i++ {
|
||||
err := store.AddAPIKey(context.Background(), &apikey.AddCommand{
|
||||
Name: fmt.Sprintf("key:%d", i),
|
||||
Key: fmt.Sprintf("key:%d", i),
|
||||
OrgId: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func testIntegrationApiKeyDataAccess(t *testing.T, fn getStore) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
@ -38,7 +60,7 @@ func TestIntegrationApiKeyDataAccess(t *testing.T) {
|
||||
|
||||
t.Run("Testing API Key data access", func(t *testing.T) {
|
||||
db := sqlstore.InitTestDB(t)
|
||||
ss := &sqlStore{db: db, cfg: db.Cfg}
|
||||
ss := fn(db)
|
||||
|
||||
t.Run("Given saved api key", func(t *testing.T) {
|
||||
cmd := apikey.AddCommand{OrgId: 1, Name: "hello", Key: "asd"}
|
||||
@ -59,6 +81,12 @@ func TestIntegrationApiKeyDataAccess(t *testing.T) {
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, key)
|
||||
})
|
||||
t.Run("Should be able to delete key by id", func(t *testing.T) {
|
||||
key, err := ss.GetAPIKeyByHash(context.Background(), cmd.Key)
|
||||
assert.NoError(t, err)
|
||||
err = ss.DeleteApiKey(context.Background(), &apikey.DeleteCommand{Id: key.Id, OrgId: key.OrgId})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Add non expiring key", func(t *testing.T) {
|
||||
@ -69,7 +97,6 @@ func TestIntegrationApiKeyDataAccess(t *testing.T) {
|
||||
query := apikey.GetByNameQuery{KeyName: "non-expiring", OrgId: 1}
|
||||
err = ss.GetApiKeyByName(context.Background(), &query)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.Nil(t, query.Result.Expires)
|
||||
})
|
||||
|
||||
@ -107,7 +134,6 @@ func TestIntegrationApiKeyDataAccess(t *testing.T) {
|
||||
query := apikey.GetByNameQuery{KeyName: "last-update-at", OrgId: 1}
|
||||
err = ss.GetApiKeyByName(context.Background(), &query)
|
||||
assert.Nil(t, err)
|
||||
|
||||
assert.NotNil(t, query.Result.LastUsedAt)
|
||||
})
|
||||
|
||||
@ -170,18 +196,10 @@ func TestIntegrationApiKeyDataAccess(t *testing.T) {
|
||||
assert.True(t, found)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationApiKeyErrors(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
mockTimeNow()
|
||||
defer resetTimeNow()
|
||||
|
||||
t.Run("Testing API Key errors", func(t *testing.T) {
|
||||
db := sqlstore.InitTestDB(t)
|
||||
ss := &sqlStore{db: db, cfg: db.Cfg}
|
||||
ss := fn(db)
|
||||
|
||||
t.Run("Delete non-existing key should return error", func(t *testing.T) {
|
||||
cmd := apikey.DeleteCommand{Id: 1}
|
||||
@ -204,65 +222,50 @@ func TestIntegrationApiKeyErrors(t *testing.T) {
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type getApiKeysTestCase struct {
|
||||
desc string
|
||||
user *user.SignedInUser
|
||||
expectedNumKeys int
|
||||
}
|
||||
|
||||
func TestIntegrationSQLStore_GetAPIKeys(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
tests := []getApiKeysTestCase{
|
||||
{
|
||||
desc: "expect all keys for wildcard scope",
|
||||
user: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
|
||||
1: {"apikeys:read": {"apikeys:*"}},
|
||||
}},
|
||||
expectedNumKeys: 10,
|
||||
},
|
||||
{
|
||||
desc: "expect only api keys that user have scopes for",
|
||||
user: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
|
||||
1: {"apikeys:read": {"apikeys:id:1", "apikeys:id:3"}},
|
||||
}},
|
||||
expectedNumKeys: 2,
|
||||
},
|
||||
{
|
||||
desc: "expect no keys when user have no scopes",
|
||||
user: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
|
||||
1: {"apikeys:read": {}},
|
||||
}},
|
||||
expectedNumKeys: 0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
db := sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{})
|
||||
store := &sqlStore{db: db, cfg: db.Cfg}
|
||||
seedApiKeys(t, store, 10)
|
||||
|
||||
query := &apikey.GetApiKeysQuery{OrgId: 1, User: tt.user}
|
||||
err := store.GetAPIKeys(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, query.Result, tt.expectedNumKeys)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func seedApiKeys(t *testing.T, store store, num int) {
|
||||
t.Helper()
|
||||
|
||||
for i := 0; i < num; i++ {
|
||||
err := store.AddAPIKey(context.Background(), &apikey.AddCommand{
|
||||
Name: fmt.Sprintf("key:%d", i),
|
||||
Key: fmt.Sprintf("key:%d", i),
|
||||
OrgId: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
t.Run("Testing Get API keys", func(t *testing.T) {
|
||||
tests := []getApiKeysTestCase{
|
||||
{
|
||||
desc: "expect all keys for wildcard scope",
|
||||
user: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
|
||||
1: {"apikeys:read": {"apikeys:*"}},
|
||||
}},
|
||||
expectedNumKeys: 10,
|
||||
expectedAllNumKeys: 10,
|
||||
},
|
||||
{
|
||||
desc: "expect only api keys that user have scopes for",
|
||||
user: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
|
||||
1: {"apikeys:read": {"apikeys:id:1", "apikeys:id:3"}},
|
||||
}},
|
||||
expectedNumKeys: 2,
|
||||
expectedAllNumKeys: 10,
|
||||
},
|
||||
{
|
||||
desc: "expect no keys when user have no scopes",
|
||||
user: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
|
||||
1: {"apikeys:read": {}},
|
||||
}},
|
||||
expectedNumKeys: 0,
|
||||
expectedAllNumKeys: 10,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
db := sqlstore.InitTestDB(t, sqlstore.InitTestDBOpt{})
|
||||
store := fn(db)
|
||||
seedApiKeys(t, store, 10)
|
||||
|
||||
query := &apikey.GetApiKeysQuery{OrgId: 1, User: tt.user}
|
||||
err := store.GetAPIKeys(context.Background(), query)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, query.Result, tt.expectedNumKeys)
|
||||
|
||||
res, err := store.GetAllAPIKeys(context.Background(), 1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.expectedAllNumKeys, len(res))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
176
pkg/services/apikey/apikeyimpl/xorm_store.go
Normal file
176
pkg/services/apikey/apikeyimpl/xorm_store.go
Normal file
@ -0,0 +1,176 @@
|
||||
package apikeyimpl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/apikey"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/db"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/pkg/errors"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type sqlStore struct {
|
||||
db db.DB
|
||||
cfg *setting.Cfg
|
||||
}
|
||||
|
||||
// timeNow makes it possible to test usage of time
|
||||
var timeNow = time.Now
|
||||
|
||||
func (ss *sqlStore) GetAPIKeys(ctx context.Context, query *apikey.GetApiKeysQuery) error {
|
||||
return ss.db.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
|
||||
var sess *xorm.Session
|
||||
|
||||
if query.IncludeExpired {
|
||||
sess = dbSession.Limit(100, 0).
|
||||
Where("org_id=?", query.OrgId).
|
||||
Asc("name")
|
||||
} else {
|
||||
sess = dbSession.Limit(100, 0).
|
||||
Where("org_id=? and ( expires IS NULL or expires >= ?)", query.OrgId, timeNow().Unix()).
|
||||
Asc("name")
|
||||
}
|
||||
|
||||
sess = sess.Where("service_account_id IS NULL")
|
||||
|
||||
if !accesscontrol.IsDisabled(ss.cfg) {
|
||||
filter, err := accesscontrol.Filter(query.User, "id", "apikeys:id:", accesscontrol.ActionAPIKeyRead)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sess.And(filter.Where, filter.Args...)
|
||||
}
|
||||
|
||||
query.Result = make([]*apikey.APIKey, 0)
|
||||
return sess.Find(&query.Result)
|
||||
})
|
||||
}
|
||||
|
||||
func (ss *sqlStore) GetAllAPIKeys(ctx context.Context, orgID int64) ([]*apikey.APIKey, error) {
|
||||
result := make([]*apikey.APIKey, 0)
|
||||
err := ss.db.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
|
||||
sess := dbSession.Where("service_account_id IS NULL").Asc("name")
|
||||
if orgID != -1 {
|
||||
sess = sess.Where("org_id=?", orgID)
|
||||
}
|
||||
return sess.Find(&result)
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (ss *sqlStore) DeleteApiKey(ctx context.Context, cmd *apikey.DeleteCommand) error {
|
||||
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
rawSQL := "DELETE FROM api_key WHERE id=? and org_id=? and service_account_id IS NULL"
|
||||
result, err := sess.Exec(rawSQL, cmd.Id, cmd.OrgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
} else if n == 0 {
|
||||
return apikey.ErrNotFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (ss *sqlStore) AddAPIKey(ctx context.Context, cmd *apikey.AddCommand) error {
|
||||
return ss.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
key := apikey.APIKey{OrgId: cmd.OrgId, Name: cmd.Name}
|
||||
exists, _ := sess.Get(&key)
|
||||
if exists {
|
||||
return apikey.ErrDuplicate
|
||||
}
|
||||
|
||||
updated := timeNow()
|
||||
var expires *int64 = nil
|
||||
if cmd.SecondsToLive > 0 {
|
||||
v := updated.Add(time.Second * time.Duration(cmd.SecondsToLive)).Unix()
|
||||
expires = &v
|
||||
} else if cmd.SecondsToLive < 0 {
|
||||
return apikey.ErrInvalidExpiration
|
||||
}
|
||||
|
||||
isRevoked := false
|
||||
t := apikey.APIKey{
|
||||
OrgId: cmd.OrgId,
|
||||
Name: cmd.Name,
|
||||
Role: cmd.Role,
|
||||
Key: cmd.Key,
|
||||
Created: updated,
|
||||
Updated: updated,
|
||||
Expires: expires,
|
||||
ServiceAccountId: cmd.ServiceAccountID,
|
||||
IsRevoked: &isRevoked,
|
||||
}
|
||||
|
||||
if _, err := sess.Insert(&t); err != nil {
|
||||
return errors.Wrap(err, "failed to insert token")
|
||||
}
|
||||
cmd.Result = &t
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (ss *sqlStore) GetApiKeyById(ctx context.Context, query *apikey.GetByIDQuery) error {
|
||||
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
var key apikey.APIKey
|
||||
has, err := sess.ID(query.ApiKeyId).Get(&key)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return apikey.ErrInvalid
|
||||
}
|
||||
|
||||
query.Result = &key
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (ss *sqlStore) GetApiKeyByName(ctx context.Context, query *apikey.GetByNameQuery) error {
|
||||
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
var key apikey.APIKey
|
||||
has, err := sess.Where("org_id=? AND name=?", query.OrgId, query.KeyName).Get(&key)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return apikey.ErrInvalid
|
||||
}
|
||||
|
||||
query.Result = &key
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (ss *sqlStore) GetAPIKeyByHash(ctx context.Context, hash string) (*apikey.APIKey, error) {
|
||||
var key apikey.APIKey
|
||||
err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
has, err := sess.Table("api_key").Where(fmt.Sprintf("%s = ?", ss.db.GetDialect().Quote("key")), hash).Get(&key)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if !has {
|
||||
return apikey.ErrInvalid
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return &key, err
|
||||
}
|
||||
|
||||
func (ss *sqlStore) UpdateAPIKeyLastUsedDate(ctx context.Context, tokenID int64) error {
|
||||
now := timeNow()
|
||||
return ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
if _, err := sess.Table("api_key").ID(tokenID).Cols("last_used_at").Update(&apikey.APIKey{LastUsedAt: &now}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
13
pkg/services/apikey/apikeyimpl/xorm_store_test.go
Normal file
13
pkg/services/apikey/apikeyimpl/xorm_store_test.go
Normal file
@ -0,0 +1,13 @@
|
||||
package apikeyimpl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
)
|
||||
|
||||
func TestIntegrationXORMApiKeyDataAccess(t *testing.T) {
|
||||
testIntegrationApiKeyDataAccess(t, func(ss *sqlstore.SQLStore) store {
|
||||
return &sqlStore{db: ss, cfg: ss.Cfg}
|
||||
})
|
||||
}
|
@ -16,8 +16,8 @@ func (s *Service) GetAPIKeys(ctx context.Context, query *apikey.GetApiKeysQuery)
|
||||
query.Result = s.ExpectedAPIKeys
|
||||
return s.ExpectedError
|
||||
}
|
||||
func (s *Service) GetAllAPIKeys(ctx context.Context, orgID int64) []*apikey.APIKey {
|
||||
return s.ExpectedAPIKeys
|
||||
func (s *Service) GetAllAPIKeys(ctx context.Context, orgID int64) ([]*apikey.APIKey, error) {
|
||||
return s.ExpectedAPIKeys, s.ExpectedError
|
||||
}
|
||||
func (s *Service) GetApiKeyById(ctx context.Context, query *apikey.GetByIDQuery) error {
|
||||
query.Result = s.ExpectedAPIKey
|
||||
|
@ -16,17 +16,17 @@ var (
|
||||
)
|
||||
|
||||
type APIKey struct {
|
||||
Id int64
|
||||
OrgId int64
|
||||
Name string
|
||||
Key string
|
||||
Role org.RoleType
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
LastUsedAt *time.Time `xorm:"last_used_at"`
|
||||
Expires *int64
|
||||
ServiceAccountId *int64
|
||||
IsRevoked *bool `xorm:"is_revoked"`
|
||||
Id int64 `db:"id"`
|
||||
OrgId int64 `db:"org_id"`
|
||||
Name string `db:"name"`
|
||||
Key string `db:"key"`
|
||||
Role org.RoleType `db:"role"`
|
||||
Created time.Time `db:"created"`
|
||||
Updated time.Time `db:"updated"`
|
||||
LastUsedAt *time.Time `xorm:"last_used_at" db:"last_used_at"`
|
||||
Expires *int64 `db:"expires"`
|
||||
ServiceAccountId *int64 `db:"service_account_id"`
|
||||
IsRevoked *bool `xorm:"is_revoked" db:"is_revoked"`
|
||||
}
|
||||
|
||||
func (k APIKey) TableName() string { return "api_key" }
|
||||
|
@ -406,7 +406,10 @@ func (s *ServiceAccountsStoreImpl) HideApiKeysTab(ctx context.Context, orgId int
|
||||
}
|
||||
|
||||
func (s *ServiceAccountsStoreImpl) MigrateApiKeysToServiceAccounts(ctx context.Context, orgId int64) error {
|
||||
basicKeys := s.apiKeyService.GetAllAPIKeys(ctx, orgId)
|
||||
basicKeys, err := s.apiKeyService.GetAllAPIKeys(ctx, orgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(basicKeys) > 0 {
|
||||
for _, key := range basicKeys {
|
||||
err := s.CreateServiceAccountFromApikey(ctx, key)
|
||||
@ -424,7 +427,10 @@ func (s *ServiceAccountsStoreImpl) MigrateApiKeysToServiceAccounts(ctx context.C
|
||||
}
|
||||
|
||||
func (s *ServiceAccountsStoreImpl) MigrateApiKey(ctx context.Context, orgId int64, keyId int64) error {
|
||||
basicKeys := s.apiKeyService.GetAllAPIKeys(ctx, orgId)
|
||||
basicKeys, err := s.apiKeyService.GetAllAPIKeys(ctx, orgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(basicKeys) == 0 {
|
||||
return fmt.Errorf("no API keys to convert found")
|
||||
}
|
||||
|
@ -341,7 +341,8 @@ func TestStore_RevertApiKey(t *testing.T) {
|
||||
// Service account should be deleted
|
||||
require.Equal(t, int64(0), serviceAccounts.TotalCount)
|
||||
|
||||
apiKeys := store.apiKeyService.GetAllAPIKeys(context.Background(), 1)
|
||||
apiKeys, err := store.apiKeyService.GetAllAPIKeys(context.Background(), 1)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apiKeys, 1)
|
||||
apiKey := apiKeys[0]
|
||||
require.Equal(t, c.key.Name, apiKey.Name)
|
||||
|
Loading…
Reference in New Issue
Block a user