diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index c8a6ac3609a..6781a76e6d6 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -218,7 +218,13 @@ func (api *ServiceAccountsAPI) SearchOrgServiceAccountsWithPaging(c *models.ReqC if page < 1 { page = 1 } - serviceAccountSearch, err := api.store.SearchOrgServiceAccounts(ctx, c.OrgId, c.Query("query"), page, perPage, c.SignedInUser) + // its okay that it fails, it is only filtering that might be weird, but to safe quard against any weird incoming query param + onlyWithExpiredTokens := c.QueryBool("expiredTokens") + filter := serviceaccounts.FilterIncludeAll + if onlyWithExpiredTokens { + filter = serviceaccounts.FilterOnlyExpiredTokens + } + serviceAccountSearch, err := api.store.SearchOrgServiceAccounts(ctx, c.OrgId, c.Query("query"), filter, page, perPage, c.SignedInUser) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to get service accounts for current organization", err) } diff --git a/pkg/services/serviceaccounts/database/database.go b/pkg/services/serviceaccounts/database/database.go index 81623aad044..21c5bc60200 100644 --- a/pkg/services/serviceaccounts/database/database.go +++ b/pkg/services/serviceaccounts/database/database.go @@ -288,7 +288,7 @@ func (s *ServiceAccountsStoreImpl) UpdateServiceAccount(ctx context.Context, } func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts( - ctx context.Context, orgID int64, query string, page int, limit int, + ctx context.Context, orgID int64, query string, filter serviceaccounts.ServiceAccountFilter, page int, limit int, signedInUser *models.SignedInUser, ) (*serviceaccounts.SearchServiceAccountsResult, error) { searchResult := &serviceaccounts.SearchServiceAccountsResult{ @@ -328,6 +328,20 @@ func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts( whereParams = append(whereParams, queryWithWildcards, queryWithWildcards, queryWithWildcards) } + switch filter { + case serviceaccounts.FilterIncludeAll: + // pass + case serviceaccounts.FilterOnlyExpiredTokens: + now := time.Now().Unix() + // we do a subquery to remove duplicates coming from joining in api_keys, if we find more than one api key that has expired + whereConditions = append( + whereConditions, + "(SELECT count(*) FROM api_key WHERE api_key.service_account_id = org_user.user_id AND api_key.expires < ?) > 0") + whereParams = append(whereParams, now) + default: + s.log.Warn("invalid filter user for service account filtering", "service account search filtering", filter) + } + if len(whereConditions) > 0 { sess.Where(strings.Join(whereConditions, " AND "), whereParams...) } diff --git a/pkg/services/serviceaccounts/models.go b/pkg/services/serviceaccounts/models.go index 5e5a4cf6de1..f350f259a90 100644 --- a/pkg/services/serviceaccounts/models.go +++ b/pkg/services/serviceaccounts/models.go @@ -60,3 +60,10 @@ type ServiceAccountProfileDTO struct { Teams []string `json:"teams" xorm:"-"` AccessControl map[string]bool `json:"accessControl,omitempty" xorm:"-"` } + +type ServiceAccountFilter string // used for filtering + +const ( + FilterOnlyExpiredTokens ServiceAccountFilter = "expiredTokens" + FilterIncludeAll ServiceAccountFilter = "all" +) diff --git a/pkg/services/serviceaccounts/serviceaccounts.go b/pkg/services/serviceaccounts/serviceaccounts.go index 5032dbe87d6..e7b186e8397 100644 --- a/pkg/services/serviceaccounts/serviceaccounts.go +++ b/pkg/services/serviceaccounts/serviceaccounts.go @@ -14,7 +14,7 @@ type Service interface { type Store interface { CreateServiceAccount(ctx context.Context, orgID int64, name string) (*ServiceAccountDTO, error) - SearchOrgServiceAccounts(ctx context.Context, orgID int64, query string, page int, limit int, + SearchOrgServiceAccounts(ctx context.Context, orgID int64, query string, filter ServiceAccountFilter, page int, limit int, signedInUser *models.SignedInUser) (*SearchServiceAccountsResult, error) UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, saForm *UpdateServiceAccountForm) (*ServiceAccountProfileDTO, error) diff --git a/pkg/services/serviceaccounts/tests/common.go b/pkg/services/serviceaccounts/tests/common.go index 1de775e5c79..6974ed1de14 100644 --- a/pkg/services/serviceaccounts/tests/common.go +++ b/pkg/services/serviceaccounts/tests/common.go @@ -123,7 +123,13 @@ func (s *ServiceAccountsStoreMock) UpdateServiceAccount(ctx context.Context, return nil, nil } -func (s *ServiceAccountsStoreMock) SearchOrgServiceAccounts(ctx context.Context, orgID int64, query string, page int, limit int, +func (s *ServiceAccountsStoreMock) SearchOrgServiceAccounts( + ctx context.Context, + orgID int64, + query string, + filter serviceaccounts.ServiceAccountFilter, + page int, + limit int, user *models.SignedInUser) (*serviceaccounts.SearchServiceAccountsResult, error) { s.Calls.SearchOrgServiceAccounts = append(s.Calls.SearchOrgServiceAccounts, []interface{}{ctx, orgID, query, page, limit, user}) return nil, nil diff --git a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx index 58c7b2b8982..14dbcca88e9 100644 --- a/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx +++ b/public/app/features/serviceaccounts/ServiceAccountsListPage.tsx @@ -21,6 +21,7 @@ import { contextSrv } from 'app/core/core'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { OrgRolePicker } from '../admin/OrgRolePicker'; import pluralize from 'pluralize'; +import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; interface OwnProps {} @@ -91,20 +92,34 @@ const ServiceAccountsListPage = ({ { label: 'All service accounts', value: false }, { label: 'Expired tokens', value: true }, ]} - onChange={(value) => changeFilter({ name: 'Expired', value })} - value={filters.find((f) => f.name === 'Expired')?.value} + onChange={(value) => changeFilter({ name: 'expiredTokens', value })} + value={filters.find((f) => f.name === 'expiredTokens')?.value} className={styles.filter} /> - {contextSrv.hasPermission(AccessControlAction.ServiceAccountsCreate) && ( - - New service account - - )} - {isLoading ? ( - - ) : ( + {isLoading && } + {!isLoading && serviceAccounts.length === 0 && ( <> + + + )} + {!isLoading && serviceAccounts.length !== 0 && ( + <> + {contextSrv.hasPermission(AccessControlAction.ServiceAccountsCreate) && ( + + New service account + + )}
diff --git a/public/app/features/serviceaccounts/state/reducers.ts b/public/app/features/serviceaccounts/state/reducers.ts index 77be98add3f..4c474b63976 100644 --- a/public/app/features/serviceaccounts/state/reducers.ts +++ b/public/app/features/serviceaccounts/state/reducers.ts @@ -44,7 +44,7 @@ export const initialStateList: ServiceAccountsState = { perPage: 50, totalPages: 1, showPaging: false, - filters: [{ name: 'Expired', value: true }], + filters: [{ name: 'expiredTokens', value: false }], }; interface ServiceAccountsFetched {