mirror of https://github.com/grafana/grafana
Separate API key store from SA token store (#45862)
* ServiceAccounts: Fix token-apikey cross deletion * ServiceAccounts: separate API key store and service account token store * ServiceAccounts: hide service account tokens from API Keys page * ServiceAccounts: uppercase statement * ServiceAccounts: fix and add new tests for SAT store * ServiceAccounts: remove service account ID from add API key * ServiceAccounts: clear up errorspull/45966/head
parent
15d681b823
commit
5cb03d6e62
@ -0,0 +1,41 @@ |
||||
package database |
||||
|
||||
import ( |
||||
"fmt" |
||||
|
||||
"github.com/grafana/grafana/pkg/models" |
||||
) |
||||
|
||||
type ErrMisingSAToken struct { |
||||
} |
||||
|
||||
func (e *ErrMisingSAToken) Error() string { |
||||
return "service account token not found" |
||||
} |
||||
|
||||
func (e *ErrMisingSAToken) Unwrap() error { |
||||
return models.ErrApiKeyNotFound |
||||
} |
||||
|
||||
type ErrInvalidExpirationSAToken struct { |
||||
} |
||||
|
||||
func (e *ErrInvalidExpirationSAToken) Error() string { |
||||
return "service account token not found" |
||||
} |
||||
|
||||
func (e *ErrInvalidExpirationSAToken) Unwrap() error { |
||||
return models.ErrInvalidApiKeyExpiration |
||||
} |
||||
|
||||
type ErrDuplicateSAToken struct { |
||||
name string |
||||
} |
||||
|
||||
func (e *ErrDuplicateSAToken) Error() string { |
||||
return fmt.Sprintf("service account token %s already exists", e.name) |
||||
} |
||||
|
||||
func (e *ErrDuplicateSAToken) Unwrap() error { |
||||
return models.ErrDuplicateApiKey |
||||
} |
||||
@ -0,0 +1,63 @@ |
||||
package database |
||||
|
||||
import ( |
||||
"context" |
||||
"time" |
||||
|
||||
"github.com/grafana/grafana/pkg/models" |
||||
"github.com/grafana/grafana/pkg/services/sqlstore" |
||||
) |
||||
|
||||
func (s *ServiceAccountsStoreImpl) AddServiceAccountToken(ctx context.Context, saID int64, cmd *models.AddApiKeyCommand) error { |
||||
return s.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { |
||||
key := models.ApiKey{OrgId: cmd.OrgId, Name: cmd.Name} |
||||
exists, _ := sess.Get(&key) |
||||
if exists { |
||||
return &ErrDuplicateSAToken{cmd.Name} |
||||
} |
||||
|
||||
updated := time.Now() |
||||
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 &ErrInvalidExpirationSAToken{} |
||||
} |
||||
|
||||
t := models.ApiKey{ |
||||
OrgId: cmd.OrgId, |
||||
Name: cmd.Name, |
||||
Role: cmd.Role, |
||||
Key: cmd.Key, |
||||
Created: updated, |
||||
Updated: updated, |
||||
Expires: expires, |
||||
ServiceAccountId: &saID, |
||||
} |
||||
|
||||
if _, err := sess.Insert(&t); err != nil { |
||||
return err |
||||
} |
||||
cmd.Result = &t |
||||
return nil |
||||
}) |
||||
} |
||||
|
||||
func (s *ServiceAccountsStoreImpl) DeleteServiceAccountToken(ctx context.Context, orgID, serviceAccountID, tokenID int64) error { |
||||
rawSQL := "DELETE FROM api_key WHERE id=? and org_id=? and service_account_id=?" |
||||
|
||||
return s.sqlStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { |
||||
result, err := sess.Exec(rawSQL, tokenID, orgID, serviceAccountID) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
n, err := result.RowsAffected() |
||||
if err != nil { |
||||
return err |
||||
} else if n == 0 { |
||||
return &ErrMisingSAToken{} |
||||
} |
||||
return nil |
||||
}) |
||||
} |
||||
@ -0,0 +1,115 @@ |
||||
package database |
||||
|
||||
import ( |
||||
"context" |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana/pkg/components/apikeygen" |
||||
"github.com/grafana/grafana/pkg/models" |
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestStore_AddServiceAccountToken(t *testing.T) { |
||||
userToCreate := tests.TestUser{Login: "servicetestwithTeam@admin", IsServiceAccount: true} |
||||
db, store := setupTestDatabase(t) |
||||
user := tests.SetupUserServiceAccount(t, db, userToCreate) |
||||
|
||||
type testCasesAdd struct { |
||||
secondsToLive int64 |
||||
desc string |
||||
} |
||||
|
||||
testCases := []testCasesAdd{{-10, "invalid"}, {0, "no expiry"}, {10, "valid"}} |
||||
|
||||
for _, tc := range testCases { |
||||
t.Run(tc.desc, func(t *testing.T) { |
||||
keyName := t.Name() |
||||
key, err := apikeygen.New(user.OrgId, keyName) |
||||
require.NoError(t, err) |
||||
|
||||
cmd := models.AddApiKeyCommand{ |
||||
Name: keyName, |
||||
Role: "Viewer", |
||||
OrgId: user.OrgId, |
||||
Key: key.HashedKey, |
||||
SecondsToLive: tc.secondsToLive, |
||||
Result: &models.ApiKey{}, |
||||
} |
||||
|
||||
err = store.AddServiceAccountToken(context.Background(), user.Id, &cmd) |
||||
if tc.secondsToLive < 0 { |
||||
require.Error(t, err) |
||||
return |
||||
} |
||||
|
||||
require.NoError(t, err) |
||||
newKey := cmd.Result |
||||
require.Equal(t, t.Name(), newKey.Name) |
||||
|
||||
// Verify against DB
|
||||
keys, errT := store.ListTokens(context.Background(), user.OrgId, user.Id) |
||||
|
||||
require.NoError(t, errT) |
||||
|
||||
found := false |
||||
for _, k := range keys { |
||||
if k.Name == keyName { |
||||
found = true |
||||
require.Equal(t, key.HashedKey, newKey.Key) |
||||
if tc.secondsToLive == 0 { |
||||
require.Nil(t, k.Expires) |
||||
} else { |
||||
require.NotNil(t, k.Expires) |
||||
} |
||||
} |
||||
} |
||||
|
||||
require.True(t, found, "Key not found") |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestStore_DeleteServiceAccountToken(t *testing.T) { |
||||
userToCreate := tests.TestUser{Login: "servicetestwithTeam@admin", IsServiceAccount: true} |
||||
db, store := setupTestDatabase(t) |
||||
user := tests.SetupUserServiceAccount(t, db, userToCreate) |
||||
|
||||
keyName := t.Name() |
||||
key, err := apikeygen.New(user.OrgId, keyName) |
||||
require.NoError(t, err) |
||||
|
||||
cmd := models.AddApiKeyCommand{ |
||||
Name: keyName, |
||||
Role: "Viewer", |
||||
OrgId: user.OrgId, |
||||
Key: key.HashedKey, |
||||
SecondsToLive: 0, |
||||
Result: &models.ApiKey{}, |
||||
} |
||||
|
||||
err = store.AddServiceAccountToken(context.Background(), user.Id, &cmd) |
||||
require.NoError(t, err) |
||||
newKey := cmd.Result |
||||
|
||||
// Delete key from wrong service account
|
||||
err = store.DeleteServiceAccountToken(context.Background(), user.OrgId, user.Id+2, newKey.Id) |
||||
require.Error(t, err) |
||||
|
||||
// Delete key from wrong org
|
||||
err = store.DeleteServiceAccountToken(context.Background(), user.OrgId+2, user.Id, newKey.Id) |
||||
require.Error(t, err) |
||||
|
||||
err = store.DeleteServiceAccountToken(context.Background(), user.OrgId, user.Id, newKey.Id) |
||||
require.NoError(t, err) |
||||
|
||||
// Verify against DB
|
||||
keys, errT := store.ListTokens(context.Background(), user.OrgId, user.Id) |
||||
require.NoError(t, errT) |
||||
|
||||
for _, k := range keys { |
||||
if k.Name == keyName { |
||||
require.Fail(t, "Key not deleted") |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue