mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Auth: Add empty role usage metrics for service and user accounts (#73108)
* Add tests for service accounts metrics usage * Add service account store implementation * Add service account service implementation * Add tests for org metrics usage * Add org implementation * Add service implementation
This commit is contained in:
parent
75b0788377
commit
1c7f89c41b
@ -12,10 +12,16 @@ func (s *ServiceAccountsStoreImpl) GetUsageMetrics(ctx context.Context) (*servic
|
|||||||
|
|
||||||
sb := &db.SQLBuilder{}
|
sb := &db.SQLBuilder{}
|
||||||
sb.Write("SELECT ")
|
sb.Write("SELECT ")
|
||||||
sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("user") +
|
sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("user") + ` ` +
|
||||||
` WHERE is_service_account = ` + dialect.BooleanStr(true) + `) AS serviceaccounts,`)
|
`WHERE is_service_account = ` + dialect.BooleanStr(true) + `) AS serviceaccounts,`)
|
||||||
sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("api_key") +
|
sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("api_key") +
|
||||||
` WHERE service_account_id IS NOT NULL ) AS serviceaccount_tokens`)
|
`WHERE service_account_id IS NOT NULL ) AS serviceaccount_tokens,`)
|
||||||
|
sb.Write(`(SELECT COUNT(*) FROM ` + dialect.Quote("org_user") + ` AS ou ` +
|
||||||
|
`JOIN ` + dialect.Quote("user") + ` AS u ON u.id = ou.user_id ` +
|
||||||
|
`WHERE u.is_disabled = ` + dialect.BooleanStr(false) + ` ` +
|
||||||
|
`AND u.is_service_account = ` + dialect.BooleanStr(true) + ` ` +
|
||||||
|
`AND ou.role=?) AS serviceaccounts_with_no_role`)
|
||||||
|
sb.AddParams("None")
|
||||||
|
|
||||||
var sqlStats serviceaccounts.Stats
|
var sqlStats serviceaccounts.Stats
|
||||||
if err := s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
|
if err := s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/components/satokengen"
|
"github.com/grafana/grafana/pkg/components/satokengen"
|
||||||
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||||
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
"github.com/grafana/grafana/pkg/services/serviceaccounts/tests"
|
||||||
)
|
)
|
||||||
@ -33,10 +34,18 @@ func TestIntegrationStore_UsageStats(t *testing.T) {
|
|||||||
_, err = store.AddServiceAccountToken(context.Background(), sa.ID, &cmd)
|
_, err = store.AddServiceAccountToken(context.Background(), sa.ID, &cmd)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
role := org.RoleNone
|
||||||
|
form := serviceaccounts.UpdateServiceAccountForm{
|
||||||
|
Role: &role,
|
||||||
|
}
|
||||||
|
_, err = store.UpdateServiceAccount(context.Background(), sa.OrgID, sa.ID, &form)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
stats, err := store.GetUsageMetrics(context.Background())
|
stats, err := store.GetUsageMetrics(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, int64(1), stats.ServiceAccounts)
|
assert.Equal(t, int64(1), stats.ServiceAccounts)
|
||||||
|
assert.Equal(t, int64(1), stats.ServiceAccountsWithNoRole)
|
||||||
assert.Equal(t, int64(1), stats.Tokens)
|
assert.Equal(t, int64(1), stats.Tokens)
|
||||||
assert.Equal(t, true, stats.ForcedExpiryEnabled)
|
assert.Equal(t, true, stats.ForcedExpiryEnabled)
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,9 @@ var (
|
|||||||
// MStatTotalServiceAccounts is a metric gauge for total number of service accounts
|
// MStatTotalServiceAccounts is a metric gauge for total number of service accounts
|
||||||
MStatTotalServiceAccounts prometheus.Gauge
|
MStatTotalServiceAccounts prometheus.Gauge
|
||||||
|
|
||||||
|
// MStatTotalServiceAccountsNoRole is a metric gauge for total number of user accounts with no role
|
||||||
|
MStatTotalServiceAccountsNoRole prometheus.Gauge
|
||||||
|
|
||||||
// MStatTotalServiceAccountTokens is a metric gauge for total number of service account tokens
|
// MStatTotalServiceAccountTokens is a metric gauge for total number of service account tokens
|
||||||
MStatTotalServiceAccountTokens prometheus.Gauge
|
MStatTotalServiceAccountTokens prometheus.Gauge
|
||||||
|
|
||||||
@ -27,6 +30,12 @@ func init() {
|
|||||||
Namespace: ExporterName,
|
Namespace: ExporterName,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
MStatTotalServiceAccountsNoRole = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
|
Name: "stat_total_service_accounts_role_none",
|
||||||
|
Help: "total amount of service accounts with no role",
|
||||||
|
Namespace: ExporterName,
|
||||||
|
})
|
||||||
|
|
||||||
MStatTotalServiceAccountTokens = prometheus.NewGauge(prometheus.GaugeOpts{
|
MStatTotalServiceAccountTokens = prometheus.NewGauge(prometheus.GaugeOpts{
|
||||||
Name: "stat_total_service_account_tokens",
|
Name: "stat_total_service_account_tokens",
|
||||||
Help: "total amount of service account tokens",
|
Help: "total amount of service account tokens",
|
||||||
@ -36,6 +45,7 @@ func init() {
|
|||||||
prometheus.MustRegister(
|
prometheus.MustRegister(
|
||||||
MStatTotalServiceAccounts,
|
MStatTotalServiceAccounts,
|
||||||
MStatTotalServiceAccountTokens,
|
MStatTotalServiceAccountTokens,
|
||||||
|
MStatTotalServiceAccountsNoRole,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,6 +58,7 @@ func (sa *ServiceAccountsService) getUsageMetrics(ctx context.Context) (map[stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
stats["stats.serviceaccounts.count"] = storeStats.ServiceAccounts
|
stats["stats.serviceaccounts.count"] = storeStats.ServiceAccounts
|
||||||
|
stats["stats.serviceaccounts.role_none.count"] = storeStats.ServiceAccountsWithNoRole
|
||||||
stats["stats.serviceaccounts.tokens.count"] = storeStats.Tokens
|
stats["stats.serviceaccounts.tokens.count"] = storeStats.Tokens
|
||||||
|
|
||||||
var forcedExpiryEnabled int64 = 0
|
var forcedExpiryEnabled int64 = 0
|
||||||
@ -64,8 +75,9 @@ func (sa *ServiceAccountsService) getUsageMetrics(ctx context.Context) (map[stri
|
|||||||
|
|
||||||
stats["stats.serviceaccounts.secret_scan.enabled.count"] = secretScanEnabled
|
stats["stats.serviceaccounts.secret_scan.enabled.count"] = secretScanEnabled
|
||||||
|
|
||||||
MStatTotalServiceAccountTokens.Set(float64(storeStats.Tokens))
|
|
||||||
MStatTotalServiceAccounts.Set(float64(storeStats.ServiceAccounts))
|
MStatTotalServiceAccounts.Set(float64(storeStats.ServiceAccounts))
|
||||||
|
MStatTotalServiceAccountsNoRole.Set(float64(storeStats.ServiceAccountsWithNoRole))
|
||||||
|
MStatTotalServiceAccountTokens.Set(float64(storeStats.Tokens))
|
||||||
|
|
||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
@ -18,15 +18,17 @@ func Test_UsageStats(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
storeMock.ExpectedStats = &serviceaccounts.Stats{
|
storeMock.ExpectedStats = &serviceaccounts.Stats{
|
||||||
ServiceAccounts: 1,
|
ServiceAccounts: 1,
|
||||||
Tokens: 1,
|
ServiceAccountsWithNoRole: 1,
|
||||||
ForcedExpiryEnabled: false,
|
Tokens: 1,
|
||||||
|
ForcedExpiryEnabled: false,
|
||||||
}
|
}
|
||||||
stats, err := svc.getUsageMetrics(context.Background())
|
stats, err := svc.getUsageMetrics(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Len(t, stats, 4, stats)
|
assert.Len(t, stats, 5, stats)
|
||||||
assert.Equal(t, int64(1), stats["stats.serviceaccounts.count"].(int64))
|
assert.Equal(t, int64(1), stats["stats.serviceaccounts.count"].(int64))
|
||||||
|
assert.Equal(t, int64(1), stats["stats.serviceaccounts.role_none.count"].(int64))
|
||||||
assert.Equal(t, int64(1), stats["stats.serviceaccounts.tokens.count"].(int64))
|
assert.Equal(t, int64(1), stats["stats.serviceaccounts.tokens.count"].(int64))
|
||||||
assert.Equal(t, int64(1), stats["stats.serviceaccounts.secret_scan.enabled.count"].(int64))
|
assert.Equal(t, int64(1), stats["stats.serviceaccounts.secret_scan.enabled.count"].(int64))
|
||||||
assert.Equal(t, int64(0), stats["stats.serviceaccounts.forced_expiry_enabled.count"].(int64))
|
assert.Equal(t, int64(0), stats["stats.serviceaccounts.forced_expiry_enabled.count"].(int64))
|
||||||
|
@ -160,9 +160,10 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Stats struct {
|
type Stats struct {
|
||||||
ServiceAccounts int64 `xorm:"serviceaccounts"`
|
ServiceAccounts int64 `xorm:"serviceaccounts"`
|
||||||
Tokens int64 `xorm:"serviceaccount_tokens"`
|
ServiceAccountsWithNoRole int64 `xorm:"serviceaccounts_with_no_role"`
|
||||||
ForcedExpiryEnabled bool `xorm:"-"`
|
Tokens int64 `xorm:"serviceaccount_tokens"`
|
||||||
|
ForcedExpiryEnabled bool `xorm:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccessEvaluator is used to protect the "Configuration > Service accounts" page access
|
// AccessEvaluator is used to protect the "Configuration > Service accounts" page access
|
||||||
|
@ -40,6 +40,7 @@ type store interface {
|
|||||||
Search(context.Context, *user.SearchUsersQuery) (*user.SearchUserQueryResult, error)
|
Search(context.Context, *user.SearchUsersQuery) (*user.SearchUserQueryResult, error)
|
||||||
|
|
||||||
Count(ctx context.Context) (int64, error)
|
Count(ctx context.Context) (int64, error)
|
||||||
|
CountUserAccountsWithEmptyRole(ctx context.Context) (int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type sqlStore struct {
|
type sqlStore struct {
|
||||||
@ -532,6 +533,27 @@ func (ss *sqlStore) Count(ctx context.Context) (int64, error) {
|
|||||||
return r.Count, err
|
return r.Count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ss *sqlStore) CountUserAccountsWithEmptyRole(ctx context.Context) (int64, error) {
|
||||||
|
sb := &db.SQLBuilder{}
|
||||||
|
sb.Write("SELECT ")
|
||||||
|
sb.Write(`(SELECT COUNT (*) from ` + ss.dialect.Quote("org_user") + ` AS ou ` +
|
||||||
|
`LEFT JOIN ` + ss.dialect.Quote("user") + ` AS u ON u.id = ou.user_id ` +
|
||||||
|
`WHERE ou.role =? ` +
|
||||||
|
`AND u.is_service_account = ` + ss.dialect.BooleanStr(false) + ` ` +
|
||||||
|
`AND u.is_disabled = ` + ss.dialect.BooleanStr(false) + `) AS user_accounts_with_no_role`)
|
||||||
|
sb.AddParams("None")
|
||||||
|
|
||||||
|
var countStats int64
|
||||||
|
if err := ss.db.WithDbSession(ctx, func(sess *db.Session) error {
|
||||||
|
_, err := sess.SQL(sb.GetSQLString(), sb.GetParams()...).Get(&countStats)
|
||||||
|
return err
|
||||||
|
}); err != nil {
|
||||||
|
return -1, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return countStats, nil
|
||||||
|
}
|
||||||
|
|
||||||
// validateOneAdminLeft validate that there is an admin user left
|
// validateOneAdminLeft validate that there is an admin user left
|
||||||
func validateOneAdminLeft(ctx context.Context, sess *db.Session) error {
|
func validateOneAdminLeft(ctx context.Context, sess *db.Session) error {
|
||||||
count, err := sess.Where("is_admin=?", true).Count(&user.User{})
|
count, err := sess.Where("is_admin=?", true).Count(&user.User{})
|
||||||
|
@ -956,6 +956,53 @@ func updateDashboardACL(t *testing.T, sqlStore db.DB, dashboardID int64, items .
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMetricsUsage(t *testing.T) {
|
||||||
|
ss := db.InitTestDB(t)
|
||||||
|
userStore := ProvideStore(ss, setting.NewCfg())
|
||||||
|
quotaService := quotaimpl.ProvideService(ss, ss.Cfg)
|
||||||
|
orgService, err := orgimpl.ProvideService(ss, ss.Cfg, quotaService)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
_, usrSvc := createOrgAndUserSvc(t, ss, ss.Cfg)
|
||||||
|
|
||||||
|
t.Run("", func(t *testing.T) {
|
||||||
|
orgId := int64(1)
|
||||||
|
|
||||||
|
// create first user
|
||||||
|
createFirtUserCmd := &user.CreateUserCommand{
|
||||||
|
Login: "admin",
|
||||||
|
Email: "admin@admin.com",
|
||||||
|
Name: "admin",
|
||||||
|
OrgID: orgId,
|
||||||
|
}
|
||||||
|
_, err := usrSvc.Create(context.Background(), createFirtUserCmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// create second user
|
||||||
|
createSecondUserCmd := &user.CreateUserCommand{
|
||||||
|
Login: "userWithoutRole",
|
||||||
|
Email: "userWithoutRole@userWithoutRole.com",
|
||||||
|
Name: "userWithoutRole",
|
||||||
|
}
|
||||||
|
secondUser, err := usrSvc.Create(context.Background(), createSecondUserCmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// assign the user to the org
|
||||||
|
cmd := org.AddOrgUserCommand{
|
||||||
|
OrgID: secondUser.OrgID,
|
||||||
|
UserID: orgId,
|
||||||
|
Role: org.RoleNone,
|
||||||
|
}
|
||||||
|
err = orgService.AddOrgUser(context.Background(), &cmd)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// get metric usage
|
||||||
|
stats, err := userStore.CountUserAccountsWithEmptyRole(context.Background())
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(1), stats)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// This function was copied from pkg/services/dashboards/database to circumvent
|
// This function was copied from pkg/services/dashboards/database to circumvent
|
||||||
// import cycles. When this org-related code is refactored into a service the
|
// import cycles. When this org-related code is refactored into a service the
|
||||||
// tests can the real GetDashboardACLInfoList functions
|
// tests can the real GetDashboardACLInfoList functions
|
||||||
|
@ -72,6 +72,14 @@ func (s *Service) GetUsageStats(ctx context.Context) map[string]interface{} {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stats["stats.case_insensitive_login.count"] = caseInsensitiveLoginVal
|
stats["stats.case_insensitive_login.count"] = caseInsensitiveLoginVal
|
||||||
|
|
||||||
|
count, err := s.store.CountUserAccountsWithEmptyRole(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
stats["stats.user.role_none.count"] = count
|
||||||
|
|
||||||
return stats
|
return stats
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,13 +193,40 @@ func TestUserService(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMetrics(t *testing.T) {
|
||||||
|
userStore := newUserStoreFake()
|
||||||
|
orgService := orgtest.NewOrgServiceFake()
|
||||||
|
|
||||||
|
userService := Service{
|
||||||
|
store: userStore,
|
||||||
|
orgService: orgService,
|
||||||
|
cacheService: localcache.ProvideService(),
|
||||||
|
teamService: &teamtest.FakeService{},
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("update user with role None", func(t *testing.T) {
|
||||||
|
userStore.ExpectedCountUserAccountsWithEmptyRoles = int64(1)
|
||||||
|
|
||||||
|
userService.cfg = setting.NewCfg()
|
||||||
|
userService.cfg.CaseInsensitiveLogin = true
|
||||||
|
|
||||||
|
stats := userService.GetUsageStats(context.Background())
|
||||||
|
assert.NotEmpty(t, stats)
|
||||||
|
|
||||||
|
assert.Len(t, stats, 2, stats)
|
||||||
|
assert.Equal(t, 1, stats["stats.case_insensitive_login.count"])
|
||||||
|
assert.Equal(t, int64(1), stats["stats.user.role_none.count"])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
type FakeUserStore struct {
|
type FakeUserStore struct {
|
||||||
ExpectedUser *user.User
|
ExpectedUser *user.User
|
||||||
ExpectedSignedInUser *user.SignedInUser
|
ExpectedSignedInUser *user.SignedInUser
|
||||||
ExpectedUserProfile *user.UserProfileDTO
|
ExpectedUserProfile *user.UserProfileDTO
|
||||||
ExpectedSearchUserQueryResult *user.SearchUserQueryResult
|
ExpectedSearchUserQueryResult *user.SearchUserQueryResult
|
||||||
ExpectedError error
|
ExpectedError error
|
||||||
ExpectedDeleteUserError error
|
ExpectedDeleteUserError error
|
||||||
|
ExpectedCountUserAccountsWithEmptyRoles int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUserStoreFake() *FakeUserStore {
|
func newUserStoreFake() *FakeUserStore {
|
||||||
@ -290,6 +317,10 @@ func (f *FakeUserStore) Count(ctx context.Context) (int64, error) {
|
|||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *FakeUserStore) CountUserAccountsWithEmptyRole(ctx context.Context) (int64, error) {
|
||||||
|
return f.ExpectedCountUserAccountsWithEmptyRoles, nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestUpdateLastSeenAt(t *testing.T) {
|
func TestUpdateLastSeenAt(t *testing.T) {
|
||||||
userStore := newUserStoreFake()
|
userStore := newUserStoreFake()
|
||||||
orgService := orgtest.NewOrgServiceFake()
|
orgService := orgtest.NewOrgServiceFake()
|
||||||
|
Loading…
Reference in New Issue
Block a user