mirror of
https://github.com/grafana/grafana.git
synced 2025-01-10 08:03:58 -06:00
Add number of tokens to the service accounts view (#44919)
* feat: add serviceaccountDTO * WIP * feat: listing number of tokens for a given service account * nit: removed fmt * Update pkg/services/serviceaccounts/database/database.go * Update public/app/features/serviceaccounts/ServiceAccountsListPage.tsx * fixes * align DTOProfile data to the frontend * reviewed myself fixes * fix: tests fix
This commit is contained in:
parent
788f77b7da
commit
79340c087f
@ -33,6 +33,11 @@ type ServiceAccountsAPI struct {
|
||||
apiKeyStore APIKeyStore
|
||||
}
|
||||
|
||||
type serviceAccountIdDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func NewServiceAccountsAPI(
|
||||
cfg *setting.Cfg,
|
||||
service serviceaccounts.Service,
|
||||
@ -88,11 +93,11 @@ func (api *ServiceAccountsAPI) CreateServiceAccount(c *models.ReqContext) respon
|
||||
case err != nil:
|
||||
return response.Error(http.StatusInternalServerError, "Failed to create service account", err)
|
||||
}
|
||||
result := models.UserIdDTO{
|
||||
Message: "Service account created",
|
||||
sa := &serviceAccountIdDTO{
|
||||
Id: user.Id,
|
||||
Message: "Service account created",
|
||||
}
|
||||
return response.JSON(http.StatusCreated, result)
|
||||
return response.JSON(http.StatusCreated, sa)
|
||||
}
|
||||
|
||||
func (api *ServiceAccountsAPI) DeleteServiceAccount(ctx *models.ReqContext) response.Response {
|
||||
|
@ -129,7 +129,7 @@ func TestServiceAccountsAPI_RetrieveServiceAccount(t *testing.T) {
|
||||
user *tests.TestUser
|
||||
expectedCode int
|
||||
acmock *accesscontrolmock.Mock
|
||||
userID int
|
||||
Id int
|
||||
}
|
||||
testCases := []testRetrieveSATestCase{
|
||||
{
|
||||
@ -157,9 +157,9 @@ func TestServiceAccountsAPI_RetrieveServiceAccount(t *testing.T) {
|
||||
expectedCode: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
desc: "should be not found when the user doesnt exist",
|
||||
user: nil,
|
||||
userID: 12,
|
||||
desc: "should be not found when the user doesnt exist",
|
||||
user: nil,
|
||||
Id: 12,
|
||||
acmock: tests.SetupMockAccesscontrol(
|
||||
t,
|
||||
func(c context.Context, siu *models.SignedInUser) ([]*accesscontrol.Permission, error) {
|
||||
@ -182,7 +182,7 @@ func TestServiceAccountsAPI_RetrieveServiceAccount(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
serviceAccountRequestScenario(t, http.MethodGet, serviceaccountIDPath, tc.user, func(httpmethod string, endpoint string, user *tests.TestUser) {
|
||||
scopeID := tc.userID
|
||||
scopeID := tc.Id
|
||||
if tc.user != nil {
|
||||
createdUser := tests.SetupUserServiceAccount(t, store, *tc.user)
|
||||
scopeID = int(createdUser.Id)
|
||||
@ -198,7 +198,7 @@ func TestServiceAccountsAPI_RetrieveServiceAccount(t *testing.T) {
|
||||
actualBody := map[string]interface{}{}
|
||||
err := json.Unmarshal(actual.Body.Bytes(), &actualBody)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, scopeID, int(actualBody["userId"].(float64)))
|
||||
require.Equal(t, scopeID, int(actualBody["id"].(float64)))
|
||||
require.Equal(t, tc.user.Login, actualBody["login"].(string))
|
||||
}
|
||||
})
|
||||
|
@ -25,7 +25,7 @@ func NewServiceAccountsStore(store *sqlstore.SQLStore) *ServiceAccountsStoreImpl
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ServiceAccountsStoreImpl) CreateServiceAccount(ctx context.Context, sa *serviceaccounts.CreateServiceaccountForm) (user *models.User, err error) {
|
||||
func (s *ServiceAccountsStoreImpl) CreateServiceAccount(ctx context.Context, sa *serviceaccounts.CreateServiceaccountForm) (saDTO *serviceaccounts.ServiceAccountDTO, err error) {
|
||||
// create a new service account - "user" with empty permissions
|
||||
generatedLogin := "Service-Account-" + uuid.New().String()
|
||||
cmd := models.CreateUserCommand{
|
||||
@ -38,7 +38,13 @@ func (s *ServiceAccountsStoreImpl) CreateServiceAccount(ctx context.Context, sa
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create user: %v", err)
|
||||
}
|
||||
return newuser, nil
|
||||
return &serviceaccounts.ServiceAccountDTO{
|
||||
Id: newuser.Id,
|
||||
Name: newuser.Name,
|
||||
Login: newuser.Login,
|
||||
OrgId: newuser.OrgId,
|
||||
Tokens: 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ServiceAccountsStoreImpl) DeleteServiceAccount(ctx context.Context, orgID, serviceaccountID int64) error {
|
||||
@ -132,33 +138,48 @@ func (s *ServiceAccountsStoreImpl) ListTokens(ctx context.Context, orgID int64,
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *ServiceAccountsStoreImpl) ListServiceAccounts(ctx context.Context, orgID, serviceAccountID int64) ([]*models.OrgUserDTO, error) {
|
||||
func (s *ServiceAccountsStoreImpl) ListServiceAccounts(ctx context.Context, orgID, serviceAccountID int64) ([]*serviceaccounts.ServiceAccountDTO, error) {
|
||||
query := models.GetOrgUsersQuery{OrgId: orgID, IsServiceAccount: true}
|
||||
if serviceAccountID > 0 {
|
||||
query.UserID = serviceAccountID
|
||||
}
|
||||
|
||||
err := s.sqlStore.GetOrgUsers(ctx, &query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return query.Result, err
|
||||
saDTOs := make([]*serviceaccounts.ServiceAccountDTO, len(query.Result))
|
||||
for i, user := range query.Result {
|
||||
saDTOs[i] = &serviceaccounts.ServiceAccountDTO{
|
||||
Id: user.UserId,
|
||||
OrgId: user.OrgId,
|
||||
Name: user.Name,
|
||||
Login: user.Login,
|
||||
}
|
||||
tokens, err := s.ListTokens(ctx, user.OrgId, user.UserId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
saDTOs[i].Tokens = int64(len(tokens))
|
||||
}
|
||||
return saDTOs, err
|
||||
}
|
||||
|
||||
// RetrieveServiceAccountByID returns a service account by its ID
|
||||
func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*models.OrgUserDTO, error) {
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(query.Result) != 1 {
|
||||
return nil, serviceaccounts.ErrServiceAccountNotFound
|
||||
}
|
||||
|
||||
return query.Result[0], err
|
||||
saProfile := &serviceaccounts.ServiceAccountProfileDTO{
|
||||
Id: query.Result[0].UserId,
|
||||
Name: query.Result[0].Name,
|
||||
Login: query.Result[0].Login,
|
||||
}
|
||||
return saProfile, err
|
||||
}
|
||||
|
||||
func contains(s []int64, e int64) bool {
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"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"
|
||||
@ -50,7 +49,7 @@ func ProvideServiceAccountsService(
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (sa *ServiceAccountsService) CreateServiceAccount(ctx context.Context, saForm *serviceaccounts.CreateServiceaccountForm) (*models.User, error) {
|
||||
func (sa *ServiceAccountsService) CreateServiceAccount(ctx context.Context, saForm *serviceaccounts.CreateServiceaccountForm) (*serviceaccounts.ServiceAccountDTO, error) {
|
||||
if !sa.features.IsEnabled(featuremgmt.FlagServiceAccounts) {
|
||||
sa.log.Debug(ServiceAccountFeatureToggleNotFound)
|
||||
return nil, nil
|
||||
|
@ -1,6 +1,10 @@
|
||||
package serviceaccounts
|
||||
|
||||
import "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
)
|
||||
|
||||
var (
|
||||
ScopeAll = "serviceaccounts:*"
|
||||
@ -22,3 +26,23 @@ type CreateServiceaccountForm struct {
|
||||
OrgID int64 `json:"-"`
|
||||
Name string `json:"name" binding:"Required"`
|
||||
}
|
||||
|
||||
type ServiceAccountDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Login string `json:"login"`
|
||||
OrgId int64 `json:"orgId"`
|
||||
Tokens int64 `json:"tokens"`
|
||||
}
|
||||
|
||||
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"`
|
||||
AccessControl map[string]bool `json:"accessControl,omitempty"`
|
||||
}
|
||||
|
@ -6,15 +6,16 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
// this should reflect the api
|
||||
type Service interface {
|
||||
CreateServiceAccount(ctx context.Context, saForm *CreateServiceaccountForm) (*models.User, error)
|
||||
CreateServiceAccount(ctx context.Context, saForm *CreateServiceaccountForm) (*ServiceAccountDTO, error)
|
||||
DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error
|
||||
}
|
||||
|
||||
type Store interface {
|
||||
CreateServiceAccount(ctx context.Context, saForm *CreateServiceaccountForm) (*models.User, error)
|
||||
ListServiceAccounts(ctx context.Context, orgID, serviceAccountID int64) ([]*models.OrgUserDTO, error)
|
||||
RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*models.OrgUserDTO, error)
|
||||
CreateServiceAccount(ctx context.Context, saForm *CreateServiceaccountForm) (*ServiceAccountDTO, error)
|
||||
ListServiceAccounts(ctx context.Context, orgID, serviceAccountID int64) ([]*ServiceAccountDTO, error)
|
||||
RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*ServiceAccountProfileDTO, error)
|
||||
DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error
|
||||
UpgradeServiceAccounts(ctx context.Context) error
|
||||
ConvertToServiceAccounts(ctx context.Context, keys []int64) error
|
||||
|
@ -29,7 +29,7 @@ func SetupUserServiceAccount(t *testing.T, sqlStore *sqlstore.SQLStore, testUser
|
||||
// create mock for serviceaccountservice
|
||||
type ServiceAccountMock struct{}
|
||||
|
||||
func (s *ServiceAccountMock) CreateServiceAccount(ctx context.Context, saForm *serviceaccounts.CreateServiceaccountForm) (*models.User, error) {
|
||||
func (s *ServiceAccountMock) CreateServiceAccount(ctx context.Context, saForm *serviceaccounts.CreateServiceaccountForm) (*serviceaccounts.ServiceAccountDTO, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@ -69,7 +69,7 @@ type ServiceAccountsStoreMock struct {
|
||||
Calls Calls
|
||||
}
|
||||
|
||||
func (s *ServiceAccountsStoreMock) CreateServiceAccount(ctx context.Context, cmd *serviceaccounts.CreateServiceaccountForm) (*models.User, error) {
|
||||
func (s *ServiceAccountsStoreMock) CreateServiceAccount(ctx context.Context, cmd *serviceaccounts.CreateServiceaccountForm) (*serviceaccounts.ServiceAccountDTO, error) {
|
||||
// now we can test that the mock has these calls when we call the function
|
||||
s.Calls.CreateServiceAccount = append(s.Calls.CreateServiceAccount, []interface{}{ctx, cmd})
|
||||
return nil, nil
|
||||
@ -95,12 +95,12 @@ func (s *ServiceAccountsStoreMock) ListTokens(ctx context.Context, orgID int64,
|
||||
s.Calls.ListTokens = append(s.Calls.ListTokens, []interface{}{ctx, orgID, serviceAccount})
|
||||
return nil, nil
|
||||
}
|
||||
func (s *ServiceAccountsStoreMock) ListServiceAccounts(ctx context.Context, orgID int64, serviceAccountID int64) ([]*models.OrgUserDTO, error) {
|
||||
func (s *ServiceAccountsStoreMock) ListServiceAccounts(ctx context.Context, orgID int64, serviceAccountID int64) ([]*serviceaccounts.ServiceAccountDTO, error) {
|
||||
s.Calls.ListServiceAccounts = append(s.Calls.ListServiceAccounts, []interface{}{ctx, orgID})
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *ServiceAccountsStoreMock) RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*models.OrgUserDTO, error) {
|
||||
func (s *ServiceAccountsStoreMock) RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*serviceaccounts.ServiceAccountProfileDTO, error) {
|
||||
s.Calls.RetrieveServiceAccount = append(s.Calls.RetrieveServiceAccount, []interface{}{ctx, orgID, serviceAccountID})
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -40,11 +40,11 @@ export function ServiceAccountProfile({
|
||||
}
|
||||
};
|
||||
|
||||
const handleServiceAccountDelete = () => onServiceAccountDelete(serviceaccount.userId);
|
||||
const handleServiceAccountDelete = () => onServiceAccountDelete(serviceaccount.id);
|
||||
|
||||
const handleServiceAccountDisable = () => onServiceAccountDisable(serviceaccount.userId);
|
||||
const handleServiceAccountDisable = () => onServiceAccountDisable(serviceaccount.id);
|
||||
|
||||
const handleServiceAccountEnable = () => onServiceAccountEnable(serviceaccount.userId);
|
||||
const handleServiceAccountEnable = () => onServiceAccountEnable(serviceaccount.id);
|
||||
|
||||
const onServiceAccountNameChange = (newValue: string) => {
|
||||
onServiceAccountUpdate({
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { memo, useEffect } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { LinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { Icon, LinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
import Page from 'app/core/components/Page/Page';
|
||||
@ -67,7 +67,7 @@ const ServiceAccountsListPage: React.FC<Props> = ({ loadServiceAccounts, navMode
|
||||
</thead>
|
||||
<tbody>
|
||||
{serviceAccounts.map((serviceaccount: ServiceAccountDTO) => (
|
||||
<ServiceAccountListItem serviceaccount={serviceaccount} key={serviceaccount.userId} />
|
||||
<ServiceAccountListItem serviceaccount={serviceaccount} key={serviceaccount.id} />
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
@ -88,11 +88,11 @@ const getServiceAccountsAriaLabel = (name: string) => {
|
||||
};
|
||||
|
||||
const ServiceAccountListItem = memo(({ serviceaccount }: ServiceAccountListItemProps) => {
|
||||
const editUrl = `org/serviceaccounts/${serviceaccount.userId}`;
|
||||
const editUrl = `org/serviceaccounts/${serviceaccount.id}`;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<tr key={serviceaccount.userId}>
|
||||
<tr key={serviceaccount.id}>
|
||||
<td className="width-4 text-center link-td">
|
||||
<a href={editUrl} aria-label={getServiceAccountsAriaLabel(serviceaccount.name)}>
|
||||
<img
|
||||
@ -143,7 +143,10 @@ const ServiceAccountListItem = memo(({ serviceaccount }: ServiceAccountListItemP
|
||||
title="tokens"
|
||||
aria-label={getServiceAccountsAriaLabel(serviceaccount.name)}
|
||||
>
|
||||
0
|
||||
<span>
|
||||
<Icon name={'key-skeleton-alt'}></Icon>
|
||||
</span>
|
||||
{serviceaccount.tokens}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -1,14 +1,13 @@
|
||||
import { ThunkResult } from '../../../types';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { ServiceAccountDTO } from 'app/types';
|
||||
import { serviceAccountLoaded, serviceAccountsLoaded, serviceAccountTokensLoaded } from './reducers';
|
||||
|
||||
const BASE_URL = `/api/serviceaccounts`;
|
||||
|
||||
export function loadServiceAccount(id: number): ThunkResult<void> {
|
||||
export function loadServiceAccount(saID: number): ThunkResult<void> {
|
||||
return async (dispatch) => {
|
||||
try {
|
||||
const response = await getBackendSrv().get(`${BASE_URL}/${id}`);
|
||||
const response = await getBackendSrv().get(`${BASE_URL}/${saID}`);
|
||||
dispatch(serviceAccountLoaded(response));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@ -45,10 +44,10 @@ export function loadServiceAccounts(): ThunkResult<void> {
|
||||
};
|
||||
}
|
||||
|
||||
export function updateServiceAccount(serviceAccount: ServiceAccountDTO): ThunkResult<void> {
|
||||
export function updateServiceAccount(saID: number): ThunkResult<void> {
|
||||
return async (dispatch) => {
|
||||
// TODO: implement on backend
|
||||
await getBackendSrv().patch(`${BASE_URL}/${serviceAccount.userId}`, {});
|
||||
await getBackendSrv().patch(`${BASE_URL}/${saID}`, {});
|
||||
dispatch(loadServiceAccounts());
|
||||
};
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ export const getServiceAccounts = (state: ServiceAccountsState) => {
|
||||
const regex = new RegExp(state.searchQuery, 'i');
|
||||
|
||||
return state.serviceAccounts.filter((serviceaccount) => {
|
||||
return regex.test(serviceaccount.login) || regex.test(serviceaccount.email) || regex.test(serviceaccount.name);
|
||||
return regex.test(serviceaccount.name) || regex.test(serviceaccount.login);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -25,15 +25,13 @@ export interface ServiceAccount {
|
||||
}
|
||||
|
||||
export interface ServiceAccountDTO extends WithAccessControlMetadata {
|
||||
id: number;
|
||||
orgId: number;
|
||||
userId: number;
|
||||
email: string;
|
||||
tokens: number;
|
||||
name: string;
|
||||
avatarUrl?: string;
|
||||
login: string;
|
||||
avatarUrl?: string;
|
||||
role: string;
|
||||
lastSeenAt: string;
|
||||
lastSeenAtAge: string;
|
||||
}
|
||||
|
||||
export interface ServiceAccountProfileState {
|
||||
|
Loading…
Reference in New Issue
Block a user