Auth: Remove api key endpoints (#106019)

* remove api key endpoints

* generate openapi specs

* remove methods from mock service

* remove ApiKeyDTO

* generate openapi specs

* remove apikey migration endpoints

* remove unused function
pull/106332/head
Mihai Doarna 7 months ago committed by GitHub
parent e78da0cc39
commit d57d184d20
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 9
      pkg/api/api.go
  2. 149
      pkg/api/apikey.go
  3. 16
      pkg/api/dtos/apikey.go
  4. 4
      pkg/services/apikey/apikey.go
  5. 16
      pkg/services/apikey/apikeyimpl/apikey.go
  6. 3
      pkg/services/apikey/apikeyimpl/store.go
  7. 51
      pkg/services/apikey/apikeyimpl/store_test.go
  8. 67
      pkg/services/apikey/apikeyimpl/xorm_store.go
  9. 10
      pkg/services/apikey/apikeytest/fake.go
  10. 11
      pkg/services/apikey/model.go
  11. 13
      pkg/services/navtree/navtreeimpl/admin.go
  12. 26
      pkg/services/serviceaccounts/api/api.go
  13. 61
      pkg/services/serviceaccounts/api/api_test.go
  14. 20
      pkg/services/serviceaccounts/database/store.go
  15. 124
      pkg/services/serviceaccounts/database/store_test.go
  16. 15
      pkg/services/serviceaccounts/manager/service.go
  17. 5
      pkg/services/serviceaccounts/manager/service_test.go
  18. 1
      pkg/services/serviceaccounts/manager/store.go
  19. 4
      pkg/services/serviceaccounts/proxy/service.go
  20. 2
      pkg/services/serviceaccounts/serviceaccounts.go
  21. 4
      pkg/services/serviceaccounts/tests/fakes.go
  22. 14
      pkg/services/serviceaccounts/tests/mocks.go
  23. 41
      public/api-enterprise-spec.json
  24. 128
      public/api-merged.json
  25. 136
      public/openapi3.json

@ -40,7 +40,6 @@ import (
"github.com/grafana/grafana/pkg/middleware/requestmeta"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/ssoutils"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/cloudmigration"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
@ -396,14 +395,6 @@ func (hs *HTTPServer) registerRoutes() {
// orgs (admin routes)
apiRoute.Get("/orgs/name/:name/", authorizeInOrg(ac.UseGlobalOrg, ac.EvalPermission(ac.ActionOrgsRead)), routing.Wrap(hs.GetOrgByName))
// auth api keys
apiRoute.Group("/auth/keys", func(keysRoute routing.RouteRegister) {
apikeyIDScope := ac.Scope("apikeys", "id", ac.Parameter(":id"))
keysRoute.Get("/", authorize(ac.EvalPermission(ac.ActionAPIKeyRead)), routing.Wrap(hs.GetAPIKeys))
keysRoute.Post("/", authorize(ac.EvalPermission(ac.ActionAPIKeyCreate)), quota(string(apikey.QuotaTargetSrv)), routing.Wrap(hs.AddAPIKey))
keysRoute.Delete("/:id", authorize(ac.EvalPermission(ac.ActionAPIKeyDelete, apikeyIDScope)), routing.Wrap(hs.DeleteAPIKey))
}, requestmeta.SetOwner(requestmeta.TeamAuth))
// Preferences
apiRoute.Group("/preferences", func(prefRoute routing.RouteRegister) {
prefRoute.Post("/set-home-dash", routing.Wrap(hs.SetHomeDashboard))

@ -1,149 +0,0 @@
package api
import (
"errors"
"net/http"
"strconv"
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/services/apikey"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/web"
)
// swagger:route GET /auth/keys api_keys getAPIkeys
//
// Get auth keys.
//
// Will return auth keys.
//
// Deprecated: true.
//
// Deprecated. Please use GET /api/serviceaccounts and GET /api/serviceaccounts/{id}/tokens instead
// see https://grafana.com/docs/grafana/next/administration/service-accounts/migrate-api-keys/.
//
// Responses:
// 200: getAPIkeyResponse
// 401: unauthorisedError
// 403: forbiddenError
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) GetAPIKeys(c *contextmodel.ReqContext) response.Response {
query := apikey.GetApiKeysQuery{OrgID: c.GetOrgID(), User: c.SignedInUser, IncludeExpired: c.QueryBool("includeExpired")}
keys, err := hs.apiKeyService.GetAPIKeys(c.Req.Context(), &query)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to list api keys", err)
}
ids := map[string]bool{}
result := make([]*dtos.ApiKeyDTO, len(keys))
for i, t := range keys {
ids[strconv.FormatInt(t.ID, 10)] = true
var expiration *time.Time = nil
if t.Expires != nil {
v := time.Unix(*t.Expires, 0)
expiration = &v
}
result[i] = &dtos.ApiKeyDTO{
ID: t.ID,
Name: t.Name,
Role: t.Role,
Expiration: expiration,
LastUsedAt: t.LastUsedAt,
}
}
metadata := getMultiAccessControlMetadata(c, "apikeys:id", ids)
if len(metadata) > 0 {
for _, key := range result {
key.AccessControl = metadata[strconv.FormatInt(key.ID, 10)]
}
}
return response.JSON(http.StatusOK, result)
}
// swagger:route DELETE /auth/keys/{id} api_keys deleteAPIkey
//
// Delete API key.
//
// Deletes an API key.
// Deprecated. See: https://grafana.com/docs/grafana/next/administration/service-accounts/migrate-api-keys/.
//
// Deprecated: true
// Responses:
// 200: okResponse
// 401: unauthorisedError
// 403: forbiddenError
// 404: notFoundError
// 500: internalServerError
func (hs *HTTPServer) DeleteAPIKey(c *contextmodel.ReqContext) response.Response {
id, err := strconv.ParseInt(web.Params(c.Req)[":id"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "id is invalid", err)
}
cmd := &apikey.DeleteCommand{ID: id, OrgID: c.GetOrgID()}
err = hs.apiKeyService.DeleteApiKey(c.Req.Context(), cmd)
if err != nil {
var status int
if errors.Is(err, apikey.ErrNotFound) {
status = http.StatusNotFound
} else {
status = http.StatusInternalServerError
}
return response.Error(status, "Failed to delete API key", err)
}
return response.Success("API key deleted")
}
// swagger:route POST /auth/keys api_keys addAPIkey
//
// Creates an API key.
//
// Will return details of the created API key.
//
// Deprecated: true
// Deprecated. Please use POST /api/serviceaccounts and POST /api/serviceaccounts/{id}/tokens
//
// see: https://grafana.com/docs/grafana/next/administration/service-accounts/migrate-api-keys/.
//
// Responses:
// 410: goneError
func (hs *HTTPServer) AddAPIKey(c *contextmodel.ReqContext) response.Response {
hs.log.Warn("Obsolete and Permanently moved API endpoint called", "path", c.Req.URL.Path)
// Respond with a 410 Gone status code
return response.Error(
http.StatusGone,
"this endpoint has been removed, please use POST /api/serviceaccounts and POST /api/serviceaccounts/{id}/tokens instead",
nil,
)
}
// swagger:parameters getAPIkeys
type GetAPIkeysParams struct {
// Show expired keys
// in:query
// required:false
// default:false
IncludeExpired bool `json:"includeExpired"`
}
// swagger:parameters deleteAPIkey
type DeleteAPIkeyParams struct {
// in:path
// required:true
ID int64 `json:"id"`
}
// swagger:response getAPIkeyResponse
type GetAPIkeyResponse struct {
// The response message
// in: body
Body []*dtos.ApiKeyDTO `json:"body"`
}

@ -1,12 +1,5 @@
package dtos
import (
"time"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/org"
)
// swagger:model
type NewApiKeyResult struct {
// example: 1
@ -16,12 +9,3 @@ type NewApiKeyResult struct {
// example: glsa_yscW25imSKJIuav8zF37RZmnbiDvB05G_fcaaf58a
Key string `json:"key"`
}
type ApiKeyDTO struct {
ID int64 `json:"id"`
Name string `json:"name"`
Role org.RoleType `json:"role"`
Expiration *time.Time `json:"expiration,omitempty"`
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
AccessControl accesscontrol.Metadata `json:"accessControl,omitempty"`
}

@ -5,13 +5,9 @@ import (
)
type Service interface {
GetAPIKeys(ctx context.Context, query *GetApiKeysQuery) (res []*APIKey, err error)
GetAllAPIKeys(ctx context.Context, orgID int64) ([]*APIKey, error)
DeleteApiKey(ctx context.Context, cmd *DeleteCommand) error
AddAPIKey(ctx context.Context, cmd *AddCommand) (res *APIKey, err error)
GetApiKeyByName(ctx context.Context, query *GetByNameQuery) (res *APIKey, err error)
GetAPIKeyByHash(ctx context.Context, hash string) (*APIKey, error)
UpdateAPIKeyLastUsedDate(ctx context.Context, tokenID int64) error
// IsDisabled returns true if the API key is not available for use.
IsDisabled(ctx context.Context, orgID int64) (bool, error)
}

@ -37,9 +37,6 @@ func (s *Service) Usage(ctx context.Context, scopeParams *quota.ScopeParameters)
return s.store.Count(ctx, scopeParams)
}
func (s *Service) GetAPIKeys(ctx context.Context, query *apikey.GetApiKeysQuery) ([]*apikey.APIKey, error) {
return s.store.GetAPIKeys(ctx, query)
}
func (s *Service) GetAllAPIKeys(ctx context.Context, orgID int64) ([]*apikey.APIKey, error) {
return s.store.GetAllAPIKeys(ctx, orgID)
}
@ -49,9 +46,6 @@ func (s *Service) GetApiKeyByName(ctx context.Context, query *apikey.GetByNameQu
func (s *Service) GetAPIKeyByHash(ctx context.Context, hash string) (*apikey.APIKey, error) {
return s.store.GetAPIKeyByHash(ctx, hash)
}
func (s *Service) DeleteApiKey(ctx context.Context, cmd *apikey.DeleteCommand) error {
return s.store.DeleteApiKey(ctx, cmd)
}
func (s *Service) AddAPIKey(ctx context.Context, cmd *apikey.AddCommand) (res *apikey.APIKey, err error) {
return s.store.AddAPIKey(ctx, cmd)
}
@ -59,16 +53,6 @@ func (s *Service) UpdateAPIKeyLastUsedDate(ctx context.Context, tokenID int64) e
return s.store.UpdateAPIKeyLastUsedDate(ctx, tokenID)
}
// IsDisabled returns true if the apikey service is disabled for the given org.
// This is the case if the org has no apikeys.
func (s *Service) IsDisabled(ctx context.Context, orgID int64) (bool, error) {
apikeys, err := s.store.CountAPIKeys(ctx, orgID)
if err != nil {
return false, err
}
return apikeys == 0, nil
}
func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
limits := &quota.Map{}

@ -8,10 +8,7 @@ import (
)
type store interface {
GetAPIKeys(ctx context.Context, query *apikey.GetApiKeysQuery) (res []*apikey.APIKey, err error)
GetAllAPIKeys(ctx context.Context, orgID int64) ([]*apikey.APIKey, error)
CountAPIKeys(ctx context.Context, orgID int64) (int64, error)
DeleteApiKey(ctx context.Context, cmd *apikey.DeleteCommand) error
AddAPIKey(ctx context.Context, cmd *apikey.AddCommand) (res *apikey.APIKey, err error)
GetApiKeyByName(ctx context.Context, query *apikey.GetByNameQuery) (res *apikey.APIKey, err error)
GetAPIKeyByHash(ctx context.Context, hash string) (*apikey.APIKey, error)

@ -11,7 +11,6 @@ import (
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/tests/testsuite"
@ -26,7 +25,6 @@ type getStore func(db.DB) store
type getApiKeysTestCase struct {
desc string
user identity.Requester
expectedNumKeys int
expectedAllNumKeys int
}
@ -86,12 +84,6 @@ func testIntegrationApiKeyDataAccess(t *testing.T, fn getStore) {
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) {
@ -171,34 +163,6 @@ func testIntegrationApiKeyDataAccess(t *testing.T, fn getStore) {
// advance mocked getTime by 1s
timeNow()
testUser := &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {accesscontrol.ActionAPIKeyRead: []string{accesscontrol.ScopeAPIKeysAll}},
},
}
query := apikey.GetApiKeysQuery{OrgID: 1, IncludeExpired: false, User: testUser}
keys, err := ss.GetAPIKeys(context.Background(), &query)
assert.Nil(t, err)
for _, k := range keys {
if k.Name == "key2" {
t.Fatalf("key2 should not be there")
}
}
query = apikey.GetApiKeysQuery{OrgID: 1, IncludeExpired: true, User: testUser}
keys, err = ss.GetAPIKeys(context.Background(), &query)
assert.Nil(t, err)
found := false
for _, k := range keys {
if k.Name == "key2" {
found = true
}
}
assert.True(t, found)
})
})
@ -206,13 +170,6 @@ func testIntegrationApiKeyDataAccess(t *testing.T, fn getStore) {
db := db.InitTestDB(t)
ss := fn(db)
t.Run("Delete non-existing key should return error", func(t *testing.T) {
cmd := apikey.DeleteCommand{ID: 1}
err := ss.DeleteApiKey(context.Background(), &cmd)
assert.EqualError(t, err, apikey.ErrNotFound.Error())
})
t.Run("Testing API Duplicate Key Errors", func(t *testing.T) {
t.Run("Given saved api key", func(t *testing.T) {
cmd := apikey.AddCommand{OrgID: 0, Name: "duplicate", Key: "asd"}
@ -235,7 +192,6 @@ func testIntegrationApiKeyDataAccess(t *testing.T, fn getStore) {
user: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
1: {"apikeys:read": {"apikeys:*"}},
}},
expectedNumKeys: 10,
expectedAllNumKeys: 10,
},
{
@ -243,7 +199,6 @@ func testIntegrationApiKeyDataAccess(t *testing.T, fn getStore) {
user: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
1: {"apikeys:read": {"apikeys:id:1", "apikeys:id:3"}},
}},
expectedNumKeys: 2,
expectedAllNumKeys: 10,
},
{
@ -251,7 +206,6 @@ func testIntegrationApiKeyDataAccess(t *testing.T, fn getStore) {
user: &user.SignedInUser{OrgID: 1, Permissions: map[int64]map[string][]string{
1: {"apikeys:read": {}},
}},
expectedNumKeys: 0,
expectedAllNumKeys: 10,
},
}
@ -262,11 +216,6 @@ func testIntegrationApiKeyDataAccess(t *testing.T, fn getStore) {
store := fn(db)
seedApiKeys(t, store, 10)
query := &apikey.GetApiKeysQuery{OrgID: 1, User: tt.user}
keys, err := store.GetAPIKeys(context.Background(), query)
require.NoError(t, err)
assert.Len(t, keys, tt.expectedNumKeys)
res, err := store.GetAllAPIKeys(context.Background(), 1)
require.NoError(t, err)
assert.Equal(t, tt.expectedAllNumKeys, len(res))

@ -5,10 +5,7 @@ import (
"fmt"
"time"
"github.com/grafana/grafana/pkg/util/xorm"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/sqlstore"
@ -21,34 +18,6 @@ type sqlStore struct {
// timeNow makes it possible to test usage of time
var timeNow = time.Now
func (ss *sqlStore) GetAPIKeys(ctx context.Context, query *apikey.GetApiKeysQuery) (res []*apikey.APIKey, err error) {
err = ss.db.WithDbSession(ctx, func(dbSession *db.Session) 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")
filter, err := accesscontrol.Filter(query.User, "id", "apikeys:id:", accesscontrol.ActionAPIKeyRead)
if err != nil {
return err
}
sess.And(filter.Where, filter.Args...)
res = make([]*apikey.APIKey, 0)
return sess.Find(&res)
})
return res, err
}
func (ss *sqlStore) GetAllAPIKeys(ctx context.Context, orgID int64) ([]*apikey.APIKey, error) {
result := make([]*apikey.APIKey, 0)
err := ss.db.WithDbSession(ctx, func(dbSession *db.Session) error {
@ -61,42 +30,6 @@ func (ss *sqlStore) GetAllAPIKeys(ctx context.Context, orgID int64) ([]*apikey.A
return result, err
}
func (ss *sqlStore) CountAPIKeys(ctx context.Context, orgID int64) (int64, error) {
type result struct {
Count int64
}
r := result{}
err := ss.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error {
rawSQL := "SELECT COUNT(*) AS count FROM api_key WHERE org_id = ? and service_account_id IS NULL"
if _, err := sess.SQL(rawSQL, orgID).Get(&r); err != nil {
return err
}
return nil
})
if err != nil {
return 0, err
}
return r.Count, err
}
func (ss *sqlStore) DeleteApiKey(ctx context.Context, cmd *apikey.DeleteCommand) error {
return ss.db.WithDbSession(ctx, func(sess *db.Session) 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) (res *apikey.APIKey, err error) {
err = ss.db.WithTransactionalDbSession(ctx, func(sess *db.Session) error {
key := apikey.APIKey{OrgID: cmd.OrgID, Name: cmd.Name}

@ -8,14 +8,10 @@ import (
type Service struct {
ExpectedError error
ExpectedBool bool
ExpectedAPIKeys []*apikey.APIKey
ExpectedAPIKey *apikey.APIKey
}
func (s *Service) GetAPIKeys(ctx context.Context, query *apikey.GetApiKeysQuery) ([]*apikey.APIKey, error) {
return s.ExpectedAPIKeys, s.ExpectedError
}
func (s *Service) GetAllAPIKeys(ctx context.Context, orgID int64) ([]*apikey.APIKey, error) {
return s.ExpectedAPIKeys, s.ExpectedError
}
@ -25,15 +21,9 @@ func (s *Service) GetApiKeyByName(ctx context.Context, query *apikey.GetByNameQu
func (s *Service) GetAPIKeyByHash(ctx context.Context, hash string) (*apikey.APIKey, error) {
return s.ExpectedAPIKey, s.ExpectedError
}
func (s *Service) DeleteApiKey(ctx context.Context, cmd *apikey.DeleteCommand) error {
return s.ExpectedError
}
func (s *Service) AddAPIKey(ctx context.Context, cmd *apikey.AddCommand) (*apikey.APIKey, error) {
return s.ExpectedAPIKey, s.ExpectedError
}
func (s *Service) UpdateAPIKeyLastUsedDate(ctx context.Context, tokenID int64) error {
return s.ExpectedError
}
func (s *Service) IsDisabled(ctx context.Context, orgID int64) (bool, error) {
return s.ExpectedBool, s.ExpectedError
}

@ -4,7 +4,6 @@ import (
"errors"
"time"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota"
)
@ -42,16 +41,6 @@ type AddCommand struct {
ServiceAccountID *int64 `json:"-"`
}
type DeleteCommand struct {
ID int64 `json:"id"`
OrgID int64 `json:"-"`
}
type GetApiKeysQuery struct {
OrgID int64
IncludeExpired bool
User identity.Requester
}
type GetByNameQuery struct {
KeyName string
OrgID int64

@ -154,19 +154,6 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
Url: s.cfg.AppSubURL + "/org/serviceaccounts",
})
}
disabled, err := s.apiKeyService.IsDisabled(ctx, c.GetOrgID())
if err != nil {
return nil, err
}
if hasAccess(ac.ApiKeyAccessEvaluator) && !disabled {
accessNodeLinks = append(accessNodeLinks, &navtree.NavLink{
Text: "API keys",
Id: "apikeys",
SubTitle: "Manage and create API keys that are used to interact with Grafana HTTP APIs",
Icon: "key-skeleton-alt",
Url: s.cfg.AppSubURL + "/org/apikeys",
})
}
if s.license.FeatureEnabled("groupsync") &&
s.features.IsEnabled(ctx, featuremgmt.FlagGroupAttributeSync) &&

@ -65,8 +65,6 @@ func (api *ServiceAccountsAPI) RegisterAPIEndpoints() {
serviceAccountsRoute.Get("/:serviceAccountId/tokens", saUIDResolver, auth(accesscontrol.EvalPermission(serviceaccounts.ActionRead, serviceaccounts.ScopeID)), routing.Wrap(api.ListTokens))
serviceAccountsRoute.Post("/:serviceAccountId/tokens", saUIDResolver, auth(accesscontrol.EvalPermission(serviceaccounts.ActionWrite, serviceaccounts.ScopeID)), routing.Wrap(api.CreateToken))
serviceAccountsRoute.Delete("/:serviceAccountId/tokens/:tokenId", saUIDResolver, auth(accesscontrol.EvalPermission(serviceaccounts.ActionWrite, serviceaccounts.ScopeID)), routing.Wrap(api.DeleteToken))
serviceAccountsRoute.Post("/migrate", auth(accesscontrol.EvalPermission(serviceaccounts.ActionCreate)), routing.Wrap(api.MigrateApiKeysToServiceAccounts))
serviceAccountsRoute.Post("/migrate/:keyId", auth(accesscontrol.EvalPermission(serviceaccounts.ActionCreate)), routing.Wrap(api.ConvertToServiceAccount))
}, requestmeta.SetOwner(requestmeta.TeamAuth))
}
@ -302,30 +300,6 @@ func (api *ServiceAccountsAPI) SearchOrgServiceAccountsWithPaging(c *contextmode
return response.JSON(http.StatusOK, serviceAccountSearch)
}
// POST /api/serviceaccounts/migrate
func (api *ServiceAccountsAPI) MigrateApiKeysToServiceAccounts(ctx *contextmodel.ReqContext) response.Response {
results, err := api.service.MigrateApiKeysToServiceAccounts(ctx.Req.Context(), ctx.GetOrgID())
if err != nil {
return response.JSON(http.StatusInternalServerError, results)
}
return response.JSON(http.StatusOK, results)
}
// POST /api/serviceaccounts/migrate/:keyId
func (api *ServiceAccountsAPI) ConvertToServiceAccount(ctx *contextmodel.ReqContext) response.Response {
keyId, err := strconv.ParseInt(web.Params(ctx.Req)[":keyId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "Key ID is invalid", err)
}
if err := api.service.MigrateApiKey(ctx.Req.Context(), ctx.GetOrgID(), keyId); err != nil {
return response.Error(http.StatusInternalServerError, "Error converting API key", err)
}
return response.Success("Service accounts migrated")
}
func (api *ServiceAccountsAPI) getAccessControlMetadata(c *contextmodel.ReqContext, saIDs map[string]bool) map[string]accesscontrol.Metadata {
if !c.QueryBool("accesscontrol") {
return map[string]accesscontrol.Metadata{}

@ -2,7 +2,6 @@ package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
@ -239,66 +238,6 @@ func TestServiceAccountsAPI_UpdateServiceAccount(t *testing.T) {
}
}
func TestServiceAccountsAPI_MigrateApiKeysToServiceAccounts(t *testing.T) {
type TestCase struct {
desc string
orgId int64
basicRole org.RoleType
permissions []accesscontrol.Permission
expectedMigrationResult *serviceaccounts.MigrationResult
expectedCode int
}
tests := []TestCase{
{
desc: "should be able to migrate API keys to service accounts with correct permissions",
orgId: 1,
basicRole: org.RoleAdmin,
permissions: []accesscontrol.Permission{
{Action: serviceaccounts.ActionCreate, Scope: serviceaccounts.ScopeAll},
},
expectedMigrationResult: &serviceaccounts.MigrationResult{
Total: 5,
Migrated: 4,
Failed: 1,
FailedDetails: []string{"API key name: failedKey - Error: migration error"},
},
expectedCode: http.StatusOK,
},
{
desc: "should not be able to migrate API keys to service accounts with wrong permissions",
orgId: 2,
basicRole: org.RoleAdmin,
permissions: []accesscontrol.Permission{
{Action: serviceaccounts.ActionCreate, Scope: serviceaccounts.ScopeAll},
},
expectedCode: http.StatusForbidden,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
server := setupTests(t, func(a *ServiceAccountsAPI) {
a.service = &satests.FakeServiceAccountService{ExpectedMigrationResult: tt.expectedMigrationResult}
})
req := server.NewRequest(http.MethodPost, "/api/serviceaccounts/migrate", nil)
webtest.RequestWithSignedInUser(req, &user.SignedInUser{OrgRole: tt.basicRole, OrgID: tt.orgId, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByActionContext(context.Background(), tt.permissions)}})
res, err := server.SendJSON(req)
require.NoError(t, err)
assert.Equal(t, tt.expectedCode, res.StatusCode)
if tt.expectedCode == http.StatusOK {
var result serviceaccounts.MigrationResult
err := json.NewDecoder(res.Body).Decode(&result)
require.NoError(t, err)
assert.Equal(t, tt.expectedMigrationResult, &result)
}
require.NoError(t, res.Body.Close())
})
}
}
func setupTests(t *testing.T, opts ...func(a *ServiceAccountsAPI)) *webtest.Server {
t.Helper()
cfg := setting.NewCfg()

@ -480,26 +480,6 @@ func (s *ServiceAccountsStoreImpl) MigrateApiKeysToServiceAccounts(ctx context.C
return migrationResult, nil
}
func (s *ServiceAccountsStoreImpl) MigrateApiKey(ctx context.Context, orgId int64, keyId int64) error {
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")
}
for _, key := range basicKeys {
if keyId == key.ID {
err := s.CreateServiceAccountFromApikey(ctx, key)
if err != nil {
s.log.Error("Converting to service account failed with error", "keyId", keyId, "error", err)
return err
}
}
}
return nil
}
func (s *ServiceAccountsStoreImpl) CreateServiceAccountFromApikey(ctx context.Context, key *apikey.APIKey) error {
prefix := "sa-autogen"
cmd := user.CreateUserCommand{

@ -2,7 +2,6 @@ package database
import (
"context"
"strings"
"testing"
"github.com/stretchr/testify/assert"
@ -320,129 +319,6 @@ func TestIntegrationStore_RetrieveServiceAccount(t *testing.T) {
}
}
func TestIntegrationStore_MigrateApiKeys(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode")
}
cases := []struct {
desc string
serviceAccounts []user.CreateUserCommand
key tests.TestApiKey
expectedLogin string
expectedErr error
}{
{
desc: "api key should be migrated to service account token",
serviceAccounts: []user.CreateUserCommand{},
key: tests.TestApiKey{Name: "test1", Role: org.RoleEditor, OrgId: 1},
expectedLogin: "sa-AuToGeN-1-test1", // Using mixed-case to test case-insensitive search.
expectedErr: nil,
},
{
desc: "api key should be migrated to service account token on second attempt",
serviceAccounts: []user.CreateUserCommand{
{Login: "sa-autogen-1-test2"},
},
key: tests.TestApiKey{Name: "test2", Role: org.RoleEditor, OrgId: 1},
expectedLogin: "sa-AuToGeN-1-test2-001", // Using mixed-case to test case-insensitive search.
expectedErr: nil,
},
{
desc: "api key should be migrated to service account token on last attempt (the 10th)",
serviceAccounts: []user.CreateUserCommand{
{Login: "sa-autogen-1-test3"},
{Login: "sa-autogen-1-test3-001"},
{Login: "sa-autogen-1-test3-002"},
{Login: "sa-autogen-1-test3-003"},
{Login: "sa-autogen-1-test3-004"},
{Login: "sa-autogen-1-test3-005"},
{Login: "sa-autogen-1-test3-006"},
{Login: "sa-autogen-1-test3-007"},
{Login: "sa-autogen-1-test3-008"},
{Login: "sa-autogen-1-test3-009"},
},
key: tests.TestApiKey{Name: "test3", Role: org.RoleEditor, OrgId: 1},
expectedLogin: "sa-AuToGeN-1-test3-010", // Using mixed-case to test case-insensitive search.
expectedErr: nil,
},
{
desc: "api key should not be migrated to service account token because all attempts failed",
serviceAccounts: []user.CreateUserCommand{
{Login: "sa-autogen-1-test4"},
{Login: "sa-autogen-1-test4-001"},
{Login: "sa-autogen-1-test4-002"},
{Login: "sa-autogen-1-test4-003"},
{Login: "sa-autogen-1-test4-004"},
{Login: "sa-autogen-1-test4-005"},
{Login: "sa-autogen-1-test4-006"},
{Login: "sa-autogen-1-test4-007"},
{Login: "sa-autogen-1-test4-008"},
{Login: "sa-autogen-1-test4-009"},
{Login: "sa-autogen-1-test4-010"},
},
key: tests.TestApiKey{Name: "test4", Role: org.RoleEditor, OrgId: 1},
expectedErr: serviceaccounts.ErrServiceAccountAlreadyExists,
},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
db, store := setupTestDatabase(t)
store.cfg.AutoAssignOrg = true
store.cfg.AutoAssignOrgId = 1
store.cfg.AutoAssignOrgRole = "Viewer"
_, err := store.orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "main"})
require.NoError(t, err)
key := tests.SetupApiKey(t, db, store.cfg, c.key)
for _, sa := range c.serviceAccounts {
sa.IsServiceAccount = true
sa.OrgID = key.OrgID
_, err := store.userService.CreateServiceAccount(context.Background(), &sa)
require.NoError(t, err)
}
err = store.MigrateApiKey(context.Background(), key.OrgID, key.ID)
if c.expectedErr != nil {
require.ErrorIs(t, err, c.expectedErr)
} else {
require.NoError(t, err)
q := serviceaccounts.SearchOrgServiceAccountsQuery{
OrgID: key.OrgID,
Query: c.expectedLogin,
Page: 1,
Limit: 50,
SignedInUser: &user.SignedInUser{
UserID: 1,
OrgID: 1,
Permissions: map[int64]map[string][]string{
key.OrgID: {
"serviceaccounts:read": {"serviceaccounts:id:*"},
},
},
},
}
serviceAccounts, err := store.SearchOrgServiceAccounts(context.Background(), &q)
require.NoError(t, err)
require.Equal(t, int64(1), serviceAccounts.TotalCount)
saMigrated := serviceAccounts.ServiceAccounts[0]
require.Equal(t, string(key.Role), saMigrated.Role)
require.Equal(t, strings.ToLower(c.expectedLogin), saMigrated.Login)
tokens, err := store.ListTokens(context.Background(), &serviceaccounts.GetSATokensQuery{
OrgID: &key.OrgID,
ServiceAccountID: &saMigrated.Id,
})
require.NoError(t, err)
require.Len(t, tokens, 1)
}
})
}
}
func TestIntegrationStore_MigrateAllApiKeys(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode")

@ -301,15 +301,6 @@ func (sa *ServiceAccountsService) DeleteServiceAccountToken(ctx context.Context,
return sa.store.DeleteServiceAccountToken(ctx, orgID, serviceAccountID, tokenID)
}
func (sa *ServiceAccountsService) MigrateApiKey(ctx context.Context, orgID, keyID int64) error {
if err := validOrgID(orgID); err != nil {
return err
}
if err := validAPIKeyID(keyID); err != nil {
return err
}
return sa.store.MigrateApiKey(ctx, orgID, keyID)
}
func (sa *ServiceAccountsService) MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) (*serviceaccounts.MigrationResult, error) {
if err := validOrgID(orgID); err != nil {
return nil, err
@ -389,9 +380,3 @@ func validServiceAccountTokenID(tokenID int64) error {
}
return nil
}
func validAPIKeyID(apiKeyID int64) error {
if apiKeyID == 0 {
return serviceaccounts.ErrServiceAccountInvalidAPIKeyID.Errorf("invalid API key ID 0 has been specified")
}
return nil
}

@ -75,11 +75,6 @@ func (f *FakeServiceAccountStore) MigrateApiKeysToServiceAccounts(ctx context.Co
return f.expectedMigratedResults, f.ExpectedError
}
// MigrateApiKey is a fake migrating an api key to a service account.
func (f *FakeServiceAccountStore) MigrateApiKey(ctx context.Context, orgID int64, keyId int64) error {
return f.ExpectedError
}
// RevertApiKey is a fake reverting an api key to a service account.
func (f *FakeServiceAccountStore) RevertApiKey(ctx context.Context, saId int64, keyId int64) error {
return f.ExpectedError

@ -15,7 +15,6 @@ type store interface {
EnableServiceAccount(ctx context.Context, orgID, serviceAccountID int64, enable bool) error
GetUsageMetrics(ctx context.Context) (*serviceaccounts.Stats, error)
ListTokens(ctx context.Context, query *serviceaccounts.GetSATokensQuery) ([]apikey.APIKey, error)
MigrateApiKey(ctx context.Context, orgID int64, keyId int64) error
MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) (*serviceaccounts.MigrationResult, error)
RetrieveServiceAccount(ctx context.Context, query *serviceaccounts.GetServiceAccountQuery) (*serviceaccounts.ServiceAccountProfileDTO, error)
RetrieveServiceAccountIdByName(ctx context.Context, orgID int64, name string) (int64, error)

@ -123,10 +123,6 @@ func (s *ServiceAccountsProxy) ListTokens(ctx context.Context, query *serviceacc
return s.proxiedService.ListTokens(ctx, query)
}
func (s *ServiceAccountsProxy) MigrateApiKey(ctx context.Context, orgID int64, keyId int64) error {
return s.proxiedService.MigrateApiKey(ctx, orgID, keyId)
}
func (s *ServiceAccountsProxy) MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) (*serviceaccounts.MigrationResult, error) {
return s.proxiedService.MigrateApiKeysToServiceAccounts(ctx, orgID)
}

@ -41,8 +41,6 @@ type Service interface {
DeleteServiceAccountToken(ctx context.Context, orgID, serviceAccountID, tokenID int64) error
ListTokens(ctx context.Context, query *GetSATokensQuery) ([]apikey.APIKey, error)
// API specific functions
MigrateApiKey(ctx context.Context, orgID int64, keyId int64) error
MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) (*MigrationResult, error)
}

@ -52,10 +52,6 @@ func (f *FakeServiceAccountService) ListTokens(ctx context.Context, query *servi
return f.ExpectedServiceAccountTokens, f.ExpectedErr
}
func (f *FakeServiceAccountService) MigrateApiKey(ctx context.Context, orgID, keyID int64) error {
return f.ExpectedErr
}
func (f *FakeServiceAccountService) MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) (*serviceaccounts.MigrationResult, error) {
return f.ExpectedMigrationResult, f.ExpectedErr
}

@ -137,20 +137,6 @@ func (_m *MockServiceAccountService) ListTokens(ctx context.Context, query *serv
return r0, r1
}
// MigrateApiKey provides a mock function with given fields: ctx, orgID, keyId
func (_m *MockServiceAccountService) MigrateApiKey(ctx context.Context, orgID int64, keyId int64) error {
ret := _m.Called(ctx, orgID, keyId)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64, int64) error); ok {
r0 = rf(ctx, orgID, keyId)
} else {
r0 = ret.Error(0)
}
return r0
}
// MigrateApiKeysToServiceAccounts provides a mock function with given fields: ctx, orgID
func (_m *MockServiceAccountService) MigrateApiKeysToServiceAccounts(ctx context.Context, orgID int64) (*serviceaccounts.MigrationResult, error) {
ret := _m.Called(ctx, orgID)

@ -3027,38 +3027,6 @@
}
}
},
"ApiKeyDTO": {
"type": "object",
"properties": {
"accessControl": {
"$ref": "#/definitions/Metadata"
},
"expiration": {
"type": "string",
"format": "date-time"
},
"id": {
"type": "integer",
"format": "int64"
},
"lastUsedAt": {
"type": "string",
"format": "date-time"
},
"name": {
"type": "string"
},
"role": {
"type": "string",
"enum": [
"None",
"Viewer",
"Editor",
"Admin"
]
}
}
},
"Assignments": {
"type": "object",
"properties": {
@ -9570,15 +9538,6 @@
"$ref": "#/definitions/ErrorResponseBody"
}
},
"getAPIkeyResponse": {
"description": "",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/ApiKeyDTO"
}
}
},
"getAccessControlStatusResponse": {
"description": "",
"schema": {

@ -2188,93 +2188,6 @@
}
}
},
"/auth/keys": {
"get": {
"description": "Will return auth keys.\n\nDeprecated: true.\n\nDeprecated. Please use GET /api/serviceaccounts and GET /api/serviceaccounts/{id}/tokens instead\nsee https://grafana.com/docs/grafana/next/administration/service-accounts/migrate-api-keys/.",
"tags": [
"api_keys"
],
"summary": "Get auth keys.",
"operationId": "getAPIkeys",
"parameters": [
{
"type": "boolean",
"default": false,
"description": "Show expired keys",
"name": "includeExpired",
"in": "query"
}
],
"responses": {
"200": {
"$ref": "#/responses/getAPIkeyResponse"
},
"401": {
"$ref": "#/responses/unauthorisedError"
},
"403": {
"$ref": "#/responses/forbiddenError"
},
"404": {
"$ref": "#/responses/notFoundError"
},
"500": {
"$ref": "#/responses/internalServerError"
}
}
},
"post": {
"description": "Will return details of the created API key.",
"tags": [
"api_keys"
],
"summary": "Creates an API key.",
"operationId": "addAPIkey",
"deprecated": true,
"responses": {
"410": {
"$ref": "#/responses/goneError"
}
}
}
},
"/auth/keys/{id}": {
"delete": {
"description": "Deletes an API key.\nDeprecated. See: https://grafana.com/docs/grafana/next/administration/service-accounts/migrate-api-keys/.",
"tags": [
"api_keys"
],
"summary": "Delete API key.",
"operationId": "deleteAPIkey",
"deprecated": true,
"parameters": [
{
"type": "integer",
"format": "int64",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"$ref": "#/responses/okResponse"
},
"401": {
"$ref": "#/responses/unauthorisedError"
},
"403": {
"$ref": "#/responses/forbiddenError"
},
"404": {
"$ref": "#/responses/notFoundError"
},
"500": {
"$ref": "#/responses/internalServerError"
}
}
}
},
"/cloudmigration/migration": {
"get": {
"tags": [
@ -13308,38 +13221,6 @@
}
}
},
"ApiKeyDTO": {
"type": "object",
"properties": {
"accessControl": {
"$ref": "#/definitions/Metadata"
},
"expiration": {
"type": "string",
"format": "date-time"
},
"id": {
"type": "integer",
"format": "int64"
},
"lastUsedAt": {
"type": "string",
"format": "date-time"
},
"name": {
"type": "string"
},
"role": {
"type": "string",
"enum": [
"None",
"Viewer",
"Editor",
"Admin"
]
}
}
},
"ApiRuleNode": {
"type": "object",
"properties": {
@ -24160,15 +24041,6 @@
"$ref": "#/definitions/ErrorResponseBody"
}
},
"getAPIkeyResponse": {
"description": "(empty)",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/ApiKeyDTO"
}
}
},
"getAccessControlStatusResponse": {
"description": "(empty)",
"schema": {

@ -628,19 +628,6 @@
},
"description": "A GenericError is the default error message that is generated.\nFor certain status codes there are more appropriate error structures."
},
"getAPIkeyResponse": {
"content": {
"application/json": {
"schema": {
"items": {
"$ref": "#/components/schemas/ApiKeyDTO"
},
"type": "array"
}
}
},
"description": "(empty)"
},
"getAccessControlStatusResponse": {
"content": {
"application/json": {
@ -3284,38 +3271,6 @@
},
"type": "object"
},
"ApiKeyDTO": {
"properties": {
"accessControl": {
"$ref": "#/components/schemas/Metadata"
},
"expiration": {
"format": "date-time",
"type": "string"
},
"id": {
"format": "int64",
"type": "integer"
},
"lastUsedAt": {
"format": "date-time",
"type": "string"
},
"name": {
"type": "string"
},
"role": {
"enum": [
"None",
"Viewer",
"Editor",
"Admin"
],
"type": "string"
}
},
"type": "object"
},
"ApiRuleNode": {
"properties": {
"alert": {
@ -16055,97 +16010,6 @@
]
}
},
"/auth/keys": {
"get": {
"description": "Will return auth keys.\n\nDeprecated: true.\n\nDeprecated. Please use GET /api/serviceaccounts and GET /api/serviceaccounts/{id}/tokens instead\nsee https://grafana.com/docs/grafana/next/administration/service-accounts/migrate-api-keys/.",
"operationId": "getAPIkeys",
"parameters": [
{
"description": "Show expired keys",
"in": "query",
"name": "includeExpired",
"schema": {
"default": false,
"type": "boolean"
}
}
],
"responses": {
"200": {
"$ref": "#/components/responses/getAPIkeyResponse"
},
"401": {
"$ref": "#/components/responses/unauthorisedError"
},
"403": {
"$ref": "#/components/responses/forbiddenError"
},
"404": {
"$ref": "#/components/responses/notFoundError"
},
"500": {
"$ref": "#/components/responses/internalServerError"
}
},
"summary": "Get auth keys.",
"tags": [
"api_keys"
]
},
"post": {
"deprecated": true,
"description": "Will return details of the created API key.",
"operationId": "addAPIkey",
"responses": {
"410": {
"$ref": "#/components/responses/goneError"
}
},
"summary": "Creates an API key.",
"tags": [
"api_keys"
]
}
},
"/auth/keys/{id}": {
"delete": {
"deprecated": true,
"description": "Deletes an API key.\nDeprecated. See: https://grafana.com/docs/grafana/next/administration/service-accounts/migrate-api-keys/.",
"operationId": "deleteAPIkey",
"parameters": [
{
"in": "path",
"name": "id",
"required": true,
"schema": {
"format": "int64",
"type": "integer"
}
}
],
"responses": {
"200": {
"$ref": "#/components/responses/okResponse"
},
"401": {
"$ref": "#/components/responses/unauthorisedError"
},
"403": {
"$ref": "#/components/responses/forbiddenError"
},
"404": {
"$ref": "#/components/responses/notFoundError"
},
"500": {
"$ref": "#/components/responses/internalServerError"
}
},
"summary": "Delete API key.",
"tags": [
"api_keys"
]
}
},
"/cloudmigration/migration": {
"get": {
"operationId": "getSessionList",

Loading…
Cancel
Save