Service Accounts: Link final components in service accounts detail page (#45929)

* ServiceAccounts: Delete/Disable service account from details page

* ServiceAccounts: capitalize viewable messages from UI

* ServiceAccounts: Link new update endpoint to details page

* ServiceAccounts: reimplement service account retrieve to include is_disabled and only target service accounts

* Cleanup styles

* Fix modal show

* ServiceAccounts: simplify handler functions

* Apply suggestions from code review

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

Co-authored-by: Clarity-89 <homes89@ukr.net>
Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
This commit is contained in:
J Guerreiro
2022-03-01 08:21:55 +00:00
committed by GitHub
parent b491d6b4dc
commit bc5237e840
10 changed files with 160 additions and 126 deletions

View File

@@ -109,12 +109,12 @@ func (api *ServiceAccountsAPI) DeleteServiceAccount(ctx *models.ReqContext) resp
if err != nil {
return response.Error(http.StatusInternalServerError, "Service account deletion error", err)
}
return response.Success("service account deleted")
return response.Success("Service account deleted")
}
func (api *ServiceAccountsAPI) UpgradeServiceAccounts(ctx *models.ReqContext) response.Response {
if err := api.store.UpgradeServiceAccounts(ctx.Req.Context()); err == nil {
return response.Success("service accounts upgraded")
return response.Success("Service accounts upgraded")
} else {
return response.Error(http.StatusInternalServerError, "Internal server error", err)
}
@@ -123,10 +123,10 @@ func (api *ServiceAccountsAPI) UpgradeServiceAccounts(ctx *models.ReqContext) re
func (api *ServiceAccountsAPI) ConvertToServiceAccount(ctx *models.ReqContext) response.Response {
keyId, err := strconv.ParseInt(web.Params(ctx.Req)[":keyId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "keyId is invalid", err)
return response.Error(http.StatusBadRequest, "Key ID is invalid", err)
}
if err := api.store.ConvertToServiceAccounts(ctx.Req.Context(), []int64{keyId}); err == nil {
return response.Success("service accounts converted")
return response.Success("Service accounts converted")
} else {
return response.Error(500, "Internal server error", err)
}
@@ -174,7 +174,7 @@ func (api *ServiceAccountsAPI) getAccessControlMetadata(c *models.ReqContext, sa
func (api *ServiceAccountsAPI) RetrieveServiceAccount(ctx *models.ReqContext) response.Response {
scopeID, err := strconv.ParseInt(web.Params(ctx.Req)[":serviceAccountId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "serviceAccountId is invalid", err)
return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err)
}
serviceAccount, err := api.store.RetrieveServiceAccount(ctx.Req.Context(), ctx.OrgId, scopeID)
@@ -197,12 +197,12 @@ func (api *ServiceAccountsAPI) RetrieveServiceAccount(ctx *models.ReqContext) re
func (api *ServiceAccountsAPI) updateServiceAccount(c *models.ReqContext) response.Response {
scopeID, err := strconv.ParseInt(web.Params(c.Req)[":serviceAccountId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "serviceAccountId is invalid", err)
return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err)
}
cmd := &serviceaccounts.UpdateServiceAccountForm{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
return response.Error(http.StatusBadRequest, "Bad request data", err)
}
if cmd.Role != nil && !cmd.Role.IsValid() {

View File

@@ -39,7 +39,7 @@ const sevenDaysAhead = 7 * 24 * time.Hour
func (api *ServiceAccountsAPI) ListTokens(ctx *models.ReqContext) response.Response {
saID, err := strconv.ParseInt(web.Params(ctx.Req)[":serviceAccountId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "serviceAccountId is invalid", err)
return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err)
}
if saTokens, err := api.store.ListTokens(ctx.Req.Context(), ctx.OrgId, saID); err == nil {
@@ -78,7 +78,7 @@ func (api *ServiceAccountsAPI) ListTokens(ctx *models.ReqContext) response.Respo
func (api *ServiceAccountsAPI) CreateToken(c *models.ReqContext) response.Response {
saID, err := strconv.ParseInt(web.Params(c.Req)[":serviceAccountId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "serviceAccountId is invalid", err)
return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err)
}
// confirm service account exists
@@ -93,7 +93,7 @@ func (api *ServiceAccountsAPI) CreateToken(c *models.ReqContext) response.Respon
cmd := models.AddApiKeyCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
return response.Error(http.StatusBadRequest, "Bad request data", err)
}
// Force affected service account to be the one referenced in the URL

View File

@@ -4,6 +4,7 @@ package database
import (
"context"
"fmt"
"strings"
"time"
"github.com/google/uuid"
@@ -169,14 +170,53 @@ func (s *ServiceAccountsStoreImpl) ListServiceAccounts(ctx context.Context, orgI
// RetrieveServiceAccountByID returns a service account by its ID
func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) {
query := models.GetOrgUsersQuery{UserID: serviceAccountID, OrgId: orgID, IsServiceAccount: true}
err := s.sqlStore.GetOrgUsers(ctx, &query)
serviceAccount := &serviceaccounts.ServiceAccountProfileDTO{}
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, 3)
whereParams := make([]interface{}, 0)
whereConditions = append(whereConditions, "org_user.org_id = ?")
whereParams = append(whereParams, orgID)
whereConditions = append(whereConditions, "org_user.user_id = ?")
whereParams = append(whereParams, serviceAccountID)
whereConditions = append(whereConditions,
fmt.Sprintf("%s.is_service_account = %s",
s.sqlStore.Dialect.Quote("user"),
s.sqlStore.Dialect.BooleanStr(true)))
sess.Where(strings.Join(whereConditions, " AND "), whereParams...)
sess.Cols(
"org_user.user_id",
"org_user.org_id",
"org_user.role",
"user.email",
"user.name",
"user.login",
"user.created",
"user.updated",
"user.is_disabled",
)
if ok, err := sess.Get(serviceAccount); err != nil {
return err
} else if !ok {
return serviceaccounts.ErrServiceAccountNotFound
}
return nil
})
if err != nil {
return nil, err
}
if len(query.Result) != 1 {
return nil, serviceaccounts.ErrServiceAccountNotFound
}
// Get Teams of service account. Can be optimized by combining with the query above
// in refactor
@@ -190,36 +230,24 @@ func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, o
teams[i] = getTeamQuery.Result[i].Name
}
saProfile := &serviceaccounts.ServiceAccountProfileDTO{
Id: query.Result[0].UserId,
Name: query.Result[0].Name,
Login: query.Result[0].Login,
OrgId: query.Result[0].OrgId,
UpdatedAt: query.Result[0].Updated,
CreatedAt: query.Result[0].Created,
Role: query.Result[0].Role,
Teams: teams,
}
return saProfile, nil
serviceAccount.Teams = teams
return serviceAccount, nil
}
func (s *ServiceAccountsStoreImpl) UpdateServiceAccount(ctx context.Context,
orgID, serviceAccountID int64,
saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) {
updatedUser := &models.OrgUserDTO{}
saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error) {
updatedUser := &serviceaccounts.ServiceAccountProfileDTO{}
err := s.sqlStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
query := models.GetOrgUsersQuery{UserID: serviceAccountID, OrgId: orgID, IsServiceAccount: true}
if err := s.sqlStore.GetOrgUsers(ctx, &query); err != nil {
var err error
updatedUser, err = s.RetrieveServiceAccount(ctx, orgID, serviceAccountID)
if err != nil {
return err
}
if len(query.Result) != 1 {
return serviceaccounts.ErrServiceAccountNotFound
}
updatedUser = query.Result[0]
if saForm.Name == nil && saForm.Role == nil {
if saForm.Name == nil && saForm.Role == nil && saForm.IsDisabled == nil {
return nil
}
@@ -236,29 +264,31 @@ func (s *ServiceAccountsStoreImpl) UpdateServiceAccount(ctx context.Context,
updatedUser.Role = string(*saForm.Role)
}
if saForm.Name != nil {
if saForm.Name != nil || saForm.IsDisabled != nil {
user := models.User{
Name: *saForm.Name,
Updated: updateTime,
}
if saForm.IsDisabled != nil {
user.IsDisabled = *saForm.IsDisabled
updatedUser.IsDisabled = *saForm.IsDisabled
sess.UseBool("is_disabled")
}
if saForm.Name != nil {
user.Name = *saForm.Name
updatedUser.Name = *saForm.Name
}
if _, err := sess.ID(serviceAccountID).Update(&user); err != nil {
return err
}
updatedUser.Name = *saForm.Name
}
return nil
})
return &serviceaccounts.ServiceAccountDTO{
Id: updatedUser.UserId,
Name: updatedUser.Name,
Login: updatedUser.Login,
Role: updatedUser.Role,
OrgId: updatedUser.OrgId,
}, err
return updatedUser, err
}
func contains(s []int64, e int64) bool {

View File

@@ -24,8 +24,9 @@ type ServiceAccount struct {
}
type UpdateServiceAccountForm struct {
Name *string `json:"name"`
Role *models.RoleType `json:"role"`
Name *string `json:"name"`
Role *models.RoleType `json:"role"`
IsDisabled *bool `json:"isDisabled"`
}
type CreateServiceAccountForm struct {
@@ -45,15 +46,15 @@ type ServiceAccountDTO struct {
}
type ServiceAccountProfileDTO struct {
Id int64 `json:"id"`
Name string `json:"name"`
Login string `json:"login"`
OrgId int64 `json:"orgId"`
IsDisabled bool `json:"isDisabled"`
UpdatedAt time.Time `json:"updatedAt"`
CreatedAt time.Time `json:"createdAt"`
AvatarUrl string `json:"avatarUrl"`
Role string `json:"role"`
Teams []string `json:"teams"`
AccessControl map[string]bool `json:"accessControl,omitempty"`
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"`
IsDisabled bool `json:"isDisabled" xorm:"is_disabled"`
Created time.Time `json:"createdAt" xorm:"created"`
Updated time.Time `json:"updatedAt" xorm:"updated"`
AvatarUrl string `json:"avatarUrl" xorm:"-"`
Role string `json:"role" xorm:"role"`
Teams []string `json:"teams" xorm:"-"`
AccessControl map[string]bool `json:"accessControl,omitempty" xorm:"-"`
}

View File

@@ -15,7 +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)
UpdateServiceAccount(ctx context.Context, orgID, serviceAccountID int64, saForm *UpdateServiceAccountForm) (*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
UpgradeServiceAccounts(ctx context.Context) error

View File

@@ -121,7 +121,7 @@ func (s *ServiceAccountsStoreMock) RetrieveServiceAccount(ctx context.Context, o
func (s *ServiceAccountsStoreMock) UpdateServiceAccount(ctx context.Context,
orgID, serviceAccountID int64,
saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountDTO, error) {
saForm *serviceaccounts.UpdateServiceAccountForm) (*serviceaccounts.ServiceAccountProfileDTO, error) {
s.Calls.UpdateServiceAccount = append(s.Calls.UpdateServiceAccount, []interface{}{ctx, orgID, serviceAccountID, saForm})
return nil, nil