Filtering of service accounts by expired tokens (#46251)

* Refactor to ServiceAccounts Query

* filtering expiredtokens on backend

* WIP

* WIP

* WIP

* fix: missing that we do not cover for no service accounts

* fix: wrong link

* feat: filter able to get only service accounts with expired tokens

* refactor: naming

* Update pkg/services/serviceaccounts/models.go

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

* goimported

* Update pkg/services/serviceaccounts/api/api.go

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
This commit is contained in:
Eric Leijonmarck 2022-03-18 15:50:34 +01:00 committed by GitHub
parent c3ee98a9b6
commit c592e6606c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 63 additions and 15 deletions

View File

@ -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)
}

View File

@ -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...)
}

View File

@ -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"
)

View File

@ -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)

View File

@ -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

View File

@ -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}
/>
</div>
{contextSrv.hasPermission(AccessControlAction.ServiceAccountsCreate) && (
<LinkButton href="org/serviceaccounts/create" variant="primary">
New service account
</LinkButton>
)}
{isLoading ? (
<PageLoader />
) : (
{isLoading && <PageLoader />}
{!isLoading && serviceAccounts.length === 0 && (
<>
<EmptyListCTA
title="You haven't created any service accounts yet."
buttonIcon="key-skeleton-alt"
buttonLink="org/serviceaccounts/create"
buttonTitle=" New service account"
buttonDisabled={!contextSrv.hasPermission(AccessControlAction.ServiceAccountsCreate)}
proTip="Remember, you can provide specific permissions for API access to other applications."
proTipLink=""
proTipLinkTitle=""
proTipTarget="_blank"
/>
</>
)}
{!isLoading && serviceAccounts.length !== 0 && (
<>
{contextSrv.hasPermission(AccessControlAction.ServiceAccountsCreate) && (
<LinkButton href="org/serviceaccounts/create" variant="primary">
New service account
</LinkButton>
)}
<div className={cx(styles.table, 'admin-list-table')}>
<table className="filter-table form-inline filter-table--hover">
<thead>

View File

@ -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 {