diff --git a/pkg/api/index.go b/pkg/api/index.go index ee06e0548d0..fd98c6b5ecb 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -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{ diff --git a/pkg/services/apikey/apikey.go b/pkg/services/apikey/apikey.go index 7e2bab5aa19..a2ac556704b 100644 --- a/pkg/services/apikey/apikey.go +++ b/pkg/services/apikey/apikey.go @@ -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 diff --git a/pkg/services/apikey/apikeyimpl/apikey.go b/pkg/services/apikey/apikeyimpl/apikey.go index 5a4a703c89f..b26b2c7634d 100644 --- a/pkg/services/apikey/apikeyimpl/apikey.go +++ b/pkg/services/apikey/apikeyimpl/apikey.go @@ -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 { diff --git a/pkg/services/apikey/apikeyimpl/sqlx_store.go b/pkg/services/apikey/apikeyimpl/sqlx_store.go new file mode 100644 index 00000000000..9401a975931 --- /dev/null +++ b/pkg/services/apikey/apikeyimpl/sqlx_store.go @@ -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 +} diff --git a/pkg/services/apikey/apikeyimpl/sqlx_store_test.go b/pkg/services/apikey/apikeyimpl/sqlx_store_test.go new file mode 100644 index 00000000000..73e33eb62ff --- /dev/null +++ b/pkg/services/apikey/apikeyimpl/sqlx_store_test.go @@ -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} + }) +} diff --git a/pkg/services/apikey/apikeyimpl/store.go b/pkg/services/apikey/apikeyimpl/store.go index c1bddd4f6e4..33b8159e7cc 100644 --- a/pkg/services/apikey/apikeyimpl/store.go +++ b/pkg/services/apikey/apikeyimpl/store.go @@ -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 - }) -} diff --git a/pkg/services/apikey/apikeyimpl/store_test.go b/pkg/services/apikey/apikeyimpl/store_test.go index 06532550b77..fcbf8602f29 100644 --- a/pkg/services/apikey/apikeyimpl/store_test.go +++ b/pkg/services/apikey/apikeyimpl/store_test.go @@ -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)) + }) + } + }) } diff --git a/pkg/services/apikey/apikeyimpl/xorm_store.go b/pkg/services/apikey/apikeyimpl/xorm_store.go new file mode 100644 index 00000000000..45453628baf --- /dev/null +++ b/pkg/services/apikey/apikeyimpl/xorm_store.go @@ -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 + }) +} diff --git a/pkg/services/apikey/apikeyimpl/xorm_store_test.go b/pkg/services/apikey/apikeyimpl/xorm_store_test.go new file mode 100644 index 00000000000..58e8d17022e --- /dev/null +++ b/pkg/services/apikey/apikeyimpl/xorm_store_test.go @@ -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} + }) +} diff --git a/pkg/services/apikey/apikeytest/fake.go b/pkg/services/apikey/apikeytest/fake.go index 360c1decec6..35ed0ec76e0 100644 --- a/pkg/services/apikey/apikeytest/fake.go +++ b/pkg/services/apikey/apikeytest/fake.go @@ -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 diff --git a/pkg/services/apikey/model.go b/pkg/services/apikey/model.go index 8e8e13ee407..82acaf3b77e 100644 --- a/pkg/services/apikey/model.go +++ b/pkg/services/apikey/model.go @@ -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" } diff --git a/pkg/services/serviceaccounts/database/database.go b/pkg/services/serviceaccounts/database/database.go index 2525223dedf..9950bbc6961 100644 --- a/pkg/services/serviceaccounts/database/database.go +++ b/pkg/services/serviceaccounts/database/database.go @@ -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") } diff --git a/pkg/services/serviceaccounts/database/database_test.go b/pkg/services/serviceaccounts/database/database_test.go index 4bd7327bf2a..85e82efd507 100644 --- a/pkg/services/serviceaccounts/database/database_test.go +++ b/pkg/services/serviceaccounts/database/database_test.go @@ -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)