mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Searchable service accounts (#45844)
* WIP * draft of WIP * feat: search and filtering works 🌈 * Update pkg/models/org_user.go * Apply suggestions from code review * refactor: remove unsed function * refactor: formatting * Apply suggestions from code review Co-authored-by: J Guerreiro <joao.guerreiro@grafana.com> * WIP * comment * Update public/app/features/serviceaccounts/ServiceAccountsListPage.tsx Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com> * review comments * wip * working search and initial load of service accounts * number of tokens working * removed api call * Apply suggestions from code review * added accescontrol param * accesscontrol prefix corrected Co-authored-by: J Guerreiro <joao.guerreiro@grafana.com> Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
This commit is contained in:
@@ -60,6 +60,7 @@ func (api *ServiceAccountsAPI) RegisterAPIEndpoints(
|
||||
auth := acmiddleware.Middleware(api.accesscontrol)
|
||||
api.RouterRegister.Group("/api/serviceaccounts", func(serviceAccountsRoute routing.RouteRegister) {
|
||||
serviceAccountsRoute.Get("/", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionRead, serviceaccounts.ScopeAll)), routing.Wrap(api.ListServiceAccounts))
|
||||
serviceAccountsRoute.Get("/search", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionRead)), routing.Wrap(api.SearchOrgServiceAccountsWithPaging))
|
||||
serviceAccountsRoute.Post("/", auth(middleware.ReqOrgAdmin,
|
||||
accesscontrol.EvalPermission(serviceaccounts.ActionCreate)), routing.Wrap(api.CreateServiceAccount))
|
||||
serviceAccountsRoute.Get("/:serviceAccountId", auth(middleware.ReqOrgAdmin,
|
||||
@@ -150,7 +151,6 @@ func (api *ServiceAccountsAPI) ListServiceAccounts(c *models.ReqContext) respons
|
||||
serviceAccounts[i].AccessControl = metadata[strconv.FormatInt(serviceAccounts[i].Id, 10)]
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusOK, serviceAccounts)
|
||||
}
|
||||
|
||||
@@ -221,3 +221,58 @@ func (api *ServiceAccountsAPI) updateServiceAccount(c *models.ReqContext) respon
|
||||
|
||||
return response.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// SearchOrgServiceAccountsWithPaging is an HTTP handler to search for org users with paging.
|
||||
// GET /api/serviceaccounts/search
|
||||
func (api *ServiceAccountsAPI) SearchOrgServiceAccountsWithPaging(c *models.ReqContext) response.Response {
|
||||
ctx := c.Req.Context()
|
||||
perPage := c.QueryInt("perpage")
|
||||
if perPage <= 0 {
|
||||
perPage = 1000
|
||||
}
|
||||
page := c.QueryInt("page")
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
query := &models.SearchOrgUsersQuery{
|
||||
OrgID: c.OrgId,
|
||||
Query: c.Query("query"),
|
||||
Page: page,
|
||||
Limit: perPage,
|
||||
User: c.SignedInUser,
|
||||
IsServiceAccount: true,
|
||||
}
|
||||
serviceAccounts, err := api.store.SearchOrgServiceAccounts(ctx, query)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusInternalServerError, "Failed to get service accounts for current organization", err)
|
||||
}
|
||||
|
||||
saIDs := map[string]bool{}
|
||||
for i := range serviceAccounts {
|
||||
serviceAccounts[i].AvatarUrl = dtos.GetGravatarUrlWithDefault("", serviceAccounts[i].Name)
|
||||
|
||||
saIDString := strconv.FormatInt(serviceAccounts[i].Id, 10)
|
||||
saIDs[saIDString] = true
|
||||
metadata := api.getAccessControlMetadata(c, map[string]bool{saIDString: true})
|
||||
serviceAccounts[i].AccessControl = metadata[strconv.FormatInt(serviceAccounts[i].Id, 10)]
|
||||
tokens, err := api.store.ListTokens(ctx, serviceAccounts[i].OrgId, serviceAccounts[i].Id)
|
||||
if err != nil {
|
||||
api.log.Warn("Failed to list tokens for service account", "serviceAccount", serviceAccounts[i].Id)
|
||||
}
|
||||
serviceAccounts[i].Tokens = int64(len(tokens))
|
||||
}
|
||||
|
||||
type searchOrgServiceAccountsQueryResult struct {
|
||||
TotalCount int64 `json:"totalCount"`
|
||||
ServiceAccounts []*serviceaccounts.ServiceAccountDTO `json:"serviceAccounts"`
|
||||
Page int `json:"page"`
|
||||
PerPage int `json:"perPage"`
|
||||
}
|
||||
result := searchOrgServiceAccountsQueryResult{
|
||||
TotalCount: query.Result.TotalCount,
|
||||
ServiceAccounts: serviceAccounts,
|
||||
Page: query.Result.Page,
|
||||
PerPage: query.Result.PerPage,
|
||||
}
|
||||
return response.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"github.com/google/uuid"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"xorm.io/xorm"
|
||||
@@ -291,6 +293,85 @@ func (s *ServiceAccountsStoreImpl) UpdateServiceAccount(ctx context.Context,
|
||||
return updatedUser, err
|
||||
}
|
||||
|
||||
func (s *ServiceAccountsStoreImpl) SearchOrgServiceAccounts(ctx context.Context, query *models.SearchOrgUsersQuery) ([]*serviceaccounts.ServiceAccountDTO, error) {
|
||||
query.IsServiceAccount = true
|
||||
|
||||
serviceAccounts := make([]*serviceaccounts.ServiceAccountDTO, 0)
|
||||
|
||||
err := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
|
||||
sess := dbSession.Table("org_user")
|
||||
sess.Join("INNER", s.sqlStore.Dialect.Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", s.sqlStore.Dialect.Quote("user")))
|
||||
|
||||
whereConditions := make([]string, 0)
|
||||
whereParams := make([]interface{}, 0)
|
||||
|
||||
whereConditions = append(whereConditions, "org_user.org_id = ?")
|
||||
whereParams = append(whereParams, query.OrgID)
|
||||
|
||||
// TODO: add to chore, for cleaning up after we have created
|
||||
// service accounts table in the modelling
|
||||
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", s.sqlStore.Dialect.Quote("user"), query.IsServiceAccount))
|
||||
|
||||
if s.sqlStore.Cfg.IsFeatureToggleEnabled(featuremgmt.FlagAccesscontrol) {
|
||||
acFilter, err := accesscontrol.Filter(ctx, "org_user.user_id", "serviceaccounts", "serviceaccounts:read", query.User)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
whereConditions = append(whereConditions, acFilter.Where)
|
||||
whereParams = append(whereParams, acFilter.Args...)
|
||||
}
|
||||
|
||||
if query.Query != "" {
|
||||
queryWithWildcards := "%" + query.Query + "%"
|
||||
whereConditions = append(whereConditions, "(email "+s.sqlStore.Dialect.LikeStr()+" ? OR name "+s.sqlStore.Dialect.LikeStr()+" ? OR login "+s.sqlStore.Dialect.LikeStr()+" ?)")
|
||||
whereParams = append(whereParams, queryWithWildcards, queryWithWildcards, queryWithWildcards)
|
||||
}
|
||||
|
||||
if len(whereConditions) > 0 {
|
||||
sess.Where(strings.Join(whereConditions, " AND "), whereParams...)
|
||||
}
|
||||
if query.Limit > 0 {
|
||||
offset := query.Limit * (query.Page - 1)
|
||||
sess.Limit(query.Limit, offset)
|
||||
}
|
||||
|
||||
sess.Cols(
|
||||
"org_user.user_id",
|
||||
"org_user.org_id",
|
||||
"org_user.role",
|
||||
"user.email",
|
||||
"user.name",
|
||||
"user.login",
|
||||
"user.last_seen_at",
|
||||
)
|
||||
sess.Asc("user.email", "user.login")
|
||||
if err := sess.Find(&serviceAccounts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get total
|
||||
serviceaccount := serviceaccounts.ServiceAccountDTO{}
|
||||
countSess := dbSession.Table("org_user")
|
||||
sess.Join("INNER", s.sqlStore.Dialect.Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", s.sqlStore.Dialect.Quote("user")))
|
||||
|
||||
if len(whereConditions) > 0 {
|
||||
countSess.Where(strings.Join(whereConditions, " AND "), whereParams...)
|
||||
}
|
||||
count, err := countSess.Count(&serviceaccount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
query.Result.TotalCount = count
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return serviceAccounts, nil
|
||||
}
|
||||
|
||||
func contains(s []int64, e int64) bool {
|
||||
for _, a := range s {
|
||||
if a == e {
|
||||
|
||||
@@ -35,12 +35,12 @@ type CreateServiceAccountForm struct {
|
||||
}
|
||||
|
||||
type ServiceAccountDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Login string `json:"login"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
Id int64 `json:"id" xorm:"user_id"`
|
||||
Name string `json:"name" xorm:"name"`
|
||||
Login string `json:"login" xorm:"login"`
|
||||
OrgId int64 `json:"orgId" xorm:"org_id"`
|
||||
Role string `json:"role" xorm:"role"`
|
||||
Tokens int64 `json:"tokens"`
|
||||
Role string `json:"role"`
|
||||
AvatarUrl string `json:"avatarUrl"`
|
||||
AccessControl map[string]bool `json:"accessControl,omitempty"`
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ type Service interface {
|
||||
type Store interface {
|
||||
CreateServiceAccount(ctx context.Context, saForm *CreateServiceAccountForm) (*ServiceAccountDTO, error)
|
||||
ListServiceAccounts(ctx context.Context, orgID, serviceAccountID int64) ([]*ServiceAccountDTO, error)
|
||||
SearchOrgServiceAccounts(ctx context.Context, query *models.SearchOrgUsersQuery) ([]*ServiceAccountDTO, error)
|
||||
UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, saForm *UpdateServiceAccountForm) (*ServiceAccountProfileDTO, error)
|
||||
RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*ServiceAccountProfileDTO, error)
|
||||
DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error
|
||||
|
||||
@@ -77,6 +77,7 @@ type Calls struct {
|
||||
DeleteServiceAccountToken []interface{}
|
||||
UpdateServiceAccount []interface{}
|
||||
AddServiceAccountToken []interface{}
|
||||
SearchOrgServiceAccounts []interface{}
|
||||
}
|
||||
|
||||
type ServiceAccountsStoreMock struct {
|
||||
@@ -127,6 +128,11 @@ func (s *ServiceAccountsStoreMock) UpdateServiceAccount(ctx context.Context,
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *ServiceAccountsStoreMock) SearchOrgServiceAccounts(ctx context.Context, query *models.SearchOrgUsersQuery) ([]*serviceaccounts.ServiceAccountDTO, error) {
|
||||
s.Calls.SearchOrgServiceAccounts = append(s.Calls.SearchOrgServiceAccounts, []interface{}{ctx, query})
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *ServiceAccountsStoreMock) DeleteServiceAccountToken(ctx context.Context, orgID, serviceAccountID, tokenID int64) error {
|
||||
s.Calls.DeleteServiceAccountToken = append(s.Calls.DeleteServiceAccountToken, []interface{}{ctx, orgID, serviceAccountID, tokenID})
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user