ServiceAccounts: Add detail view of service account (#44164)

* ServiceAccounts: Add detail view of service account

Co-authored-by: eleijonmarck <eric.leijonmarck@gmail.com>

* ServiceAccount: Make detail view scopeID

Co-authored-by: eleijonmarck <eric.leijonmarck@gmail.com>

* ServiceAccount: fix lint error

Co-authored-by: eleijonmarck <eric.leijonmarck@gmail.com>

Co-authored-by: eleijonmarck <eric.leijonmarck@gmail.com>
This commit is contained in:
J Guerreiro 2022-01-19 09:23:46 +00:00 committed by GitHub
parent 8917e66eb4
commit 7dab52869e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 176 additions and 11 deletions

View File

@ -107,6 +107,7 @@ type UpdateOrgUserCommand struct {
// QUERIES
type GetOrgUsersQuery struct {
UserID int64
OrgId int64
Query string
Limit int

View File

@ -48,6 +48,7 @@ func (api *ServiceAccountsAPI) RegisterAPIEndpoints(
auth := acmiddleware.Middleware(api.accesscontrol)
api.RouterRegister.Group("/api/org/serviceaccounts", func(serviceAccountsRoute routing.RouteRegister) {
serviceAccountsRoute.Get("/", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionRead, serviceaccounts.ScopeAll)), routing.Wrap(api.ListServiceAccounts))
serviceAccountsRoute.Get("/:serviceAccountId", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionRead, serviceaccounts.ScopeID)), routing.Wrap(api.RetrieveServiceAccount))
serviceAccountsRoute.Delete("/:serviceAccountId", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionDelete, serviceaccounts.ScopeID)), routing.Wrap(api.DeleteServiceAccount))
serviceAccountsRoute.Get("/upgrade", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionCreate, serviceaccounts.ScopeID)), routing.Wrap(api.UpgradeServiceAccounts))
serviceAccountsRoute.Post("/", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionCreate, serviceaccounts.ScopeID)), routing.Wrap(api.CreateServiceAccount))
@ -122,7 +123,25 @@ func (api *ServiceAccountsAPI) ListTokens(ctx *models.ReqContext) response.Respo
func (api *ServiceAccountsAPI) ListServiceAccounts(ctx *models.ReqContext) response.Response {
serviceAccounts, err := api.store.ListServiceAccounts(ctx.Req.Context(), ctx.OrgId)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to list roles", err)
return response.Error(http.StatusInternalServerError, "Failed to list service accounts", err)
}
return response.JSON(http.StatusOK, serviceAccounts)
}
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)
}
serviceAccount, err := api.store.RetrieveServiceAccount(ctx.Req.Context(), ctx.OrgId, scopeID)
if err != nil {
switch {
case errors.Is(err, serviceaccounts.ErrServiceAccountNotFound):
return response.Error(http.StatusNotFound, "Failed to retrieve service account", err)
default:
return response.Error(http.StatusInternalServerError, "Failed to retrieve service account", err)
}
}
return response.JSON(http.StatusOK, serviceAccount)
}

View File

@ -2,6 +2,7 @@ package api
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
@ -23,7 +24,7 @@ import (
)
var (
serviceaccountIDPath = "/api/org/serviceaccounts/%s"
serviceaccountIDPath = "/api/org/serviceaccounts/%v"
)
// test the accesscontrol endpoints
@ -56,9 +57,9 @@ func TestServiceAccountsAPI_DeleteServiceAccount(t *testing.T) {
),
expectedCode: http.StatusOK,
}
serviceAccountDeletionScenario(t, http.MethodDelete, serviceaccountIDPath, &testcase.user, func(httpmethod string, endpoint string, user *tests.TestUser) {
serviceAccountRequestScenario(t, http.MethodDelete, serviceaccountIDPath, &testcase.user, func(httpmethod string, endpoint string, user *tests.TestUser) {
createduser := tests.SetupUserServiceAccount(t, store, testcase.user)
server := setupTestServer(t, &svcmock, routing.NewRouteRegister(), testcase.acmock)
server := setupTestServer(t, &svcmock, routing.NewRouteRegister(), testcase.acmock, store)
actual := requestResponse(server, httpmethod, fmt.Sprintf(endpoint, fmt.Sprint(createduser.Id))).Code
require.Equal(t, testcase.expectedCode, actual)
})
@ -80,23 +81,22 @@ func TestServiceAccountsAPI_DeleteServiceAccount(t *testing.T) {
),
expectedCode: http.StatusForbidden,
}
serviceAccountDeletionScenario(t, http.MethodDelete, serviceaccountIDPath, &testcase.user, func(httpmethod string, endpoint string, user *tests.TestUser) {
serviceAccountRequestScenario(t, http.MethodDelete, serviceaccountIDPath, &testcase.user, func(httpmethod string, endpoint string, user *tests.TestUser) {
createduser := tests.SetupUserServiceAccount(t, store, testcase.user)
server := setupTestServer(t, &svcmock, routing.NewRouteRegister(), testcase.acmock)
actual := requestResponse(server, httpmethod, fmt.Sprintf(endpoint, fmt.Sprint(createduser.Id))).Code
server := setupTestServer(t, &svcmock, routing.NewRouteRegister(), testcase.acmock, store)
actual := requestResponse(server, httpmethod, fmt.Sprintf(endpoint, createduser.Id)).Code
require.Equal(t, testcase.expectedCode, actual)
})
})
}
func serviceAccountDeletionScenario(t *testing.T, httpMethod string, endpoint string, user *tests.TestUser, fn func(httpmethod string, endpoint string, user *tests.TestUser)) {
func serviceAccountRequestScenario(t *testing.T, httpMethod string, endpoint string, user *tests.TestUser, fn func(httpmethod string, endpoint string, user *tests.TestUser)) {
t.Helper()
fn(httpMethod, endpoint, user)
}
func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, routerRegister routing.RouteRegister, acmock *accesscontrolmock.Mock) *web.Mux {
store := sqlstore.InitTestDB(t)
a := NewServiceAccountsAPI(svc, acmock, routerRegister, database.NewServiceAccountsStore(store))
func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, routerRegister routing.RouteRegister, acmock *accesscontrolmock.Mock, sqlStore *sqlstore.SQLStore) *web.Mux {
a := NewServiceAccountsAPI(svc, acmock, routerRegister, database.NewServiceAccountsStore(sqlStore))
a.RegisterAPIEndpoints(&setting.Cfg{FeatureToggles: map[string]bool{"service-accounts": true}})
m := web.New()
@ -117,3 +117,88 @@ func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, routerRegister
a.RouterRegister.Register(m.Router)
return m
}
func TestServiceAccountsAPI_RetrieveServiceAccount(t *testing.T) {
store := sqlstore.InitTestDB(t)
svcmock := tests.ServiceAccountMock{}
type testRetrieveSATestCase struct {
desc string
user *tests.TestUser
expectedCode int
acmock *accesscontrolmock.Mock
userID int
}
testCases := []testRetrieveSATestCase{
{
desc: "should be ok to retrieve serviceaccount with permissions",
user: &tests.TestUser{Login: "servicetest1@admin", IsServiceAccount: true},
acmock: tests.SetupMockAccesscontrol(
t,
func(c context.Context, siu *models.SignedInUser) ([]*accesscontrol.Permission, error) {
return []*accesscontrol.Permission{{Action: serviceaccounts.ActionRead, Scope: serviceaccounts.ScopeAll}}, nil
},
false,
),
expectedCode: http.StatusOK,
},
{
desc: "should be forbidden to retrieve serviceaccount if no permissions",
user: &tests.TestUser{Login: "servicetest2@admin", IsServiceAccount: true},
acmock: tests.SetupMockAccesscontrol(
t,
func(c context.Context, siu *models.SignedInUser) ([]*accesscontrol.Permission, error) {
return []*accesscontrol.Permission{}, nil
},
false,
),
expectedCode: http.StatusForbidden,
},
{
desc: "should be not found when the user doesnt exist",
user: nil,
userID: 12,
acmock: tests.SetupMockAccesscontrol(
t,
func(c context.Context, siu *models.SignedInUser) ([]*accesscontrol.Permission, error) {
return []*accesscontrol.Permission{{Action: serviceaccounts.ActionRead, Scope: serviceaccounts.ScopeAll}}, nil
},
false,
),
expectedCode: http.StatusNotFound,
},
}
var requestResponse = func(server *web.Mux, httpMethod, requestpath string) *httptest.ResponseRecorder {
req, err := http.NewRequest(httpMethod, requestpath, nil)
require.NoError(t, err)
recorder := httptest.NewRecorder()
server.ServeHTTP(recorder, req)
return recorder
}
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
if tc.user != nil {
createdUser := tests.SetupUserServiceAccount(t, store, *tc.user)
scopeID = int(createdUser.Id)
}
server := setupTestServer(t, &svcmock, routing.NewRouteRegister(), tc.acmock, store)
actual := requestResponse(server, httpmethod, fmt.Sprintf(endpoint, scopeID))
actualCode := actual.Code
require.Equal(t, tc.expectedCode, actualCode)
if actualCode == http.StatusOK {
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, tc.user.Login, actualBody["login"].(string))
}
})
})
}
}

View File

@ -111,3 +111,18 @@ func (s *ServiceAccountsStoreImpl) ListServiceAccounts(ctx context.Context, orgI
}
return query.Result, err
}
// RetrieveServiceAccountByID returns a service account by its ID
func (s *ServiceAccountsStoreImpl) RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*models.OrgUserDTO, 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
}

View File

@ -47,3 +47,36 @@ func setupTestDatabase(t *testing.T) (*sqlstore.SQLStore, *ServiceAccountsStoreI
db := sqlstore.InitTestDB(t)
return db, NewServiceAccountsStore(db)
}
func TestStore_RetrieveServiceAccount(t *testing.T) {
cases := []struct {
desc string
user tests.TestUser
expectedErr error
}{
{
desc: "service accounts should exist and get retrieved",
user: tests.TestUser{Login: "servicetest1@admin", IsServiceAccount: true},
expectedErr: nil,
},
{
desc: "service accounts is false should not retrieve user",
user: tests.TestUser{Login: "test1@admin", IsServiceAccount: false},
expectedErr: serviceaccounts.ErrServiceAccountNotFound,
},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
db, store := setupTestDatabase(t)
user := tests.SetupUserServiceAccount(t, db, c.user)
dto, err := store.RetrieveServiceAccount(context.Background(), user.OrgId, user.Id)
if c.expectedErr != nil {
require.ErrorIs(t, err, c.expectedErr)
} else {
require.NoError(t, err)
require.Equal(t, c.user.Login, dto.Login)
}
})
}
}

View File

@ -15,6 +15,7 @@ type Service interface {
type Store interface {
CreateServiceAccount(ctx context.Context, saForm *CreateServiceaccountForm) (*models.User, error)
ListServiceAccounts(ctx context.Context, orgID int64) ([]*models.OrgUserDTO, error)
RetrieveServiceAccount(ctx context.Context, orgID, serviceAccountID int64) (*models.OrgUserDTO, error)
DeleteServiceAccount(ctx context.Context, orgID, serviceAccountID int64) error
UpgradeServiceAccounts(ctx context.Context) error
ListTokens(ctx context.Context, orgID int64, serviceAccount int64) ([]*models.ApiKey, error)

View File

@ -58,6 +58,7 @@ var _ serviceaccounts.Store = new(ServiceAccountsStoreMock)
type Calls struct {
CreateServiceAccount []interface{}
ListServiceAccounts []interface{}
RetrieveServiceAccount []interface{}
DeleteServiceAccount []interface{}
UpgradeServiceAccounts []interface{}
ListTokens []interface{}
@ -92,3 +93,8 @@ func (s *ServiceAccountsStoreMock) ListServiceAccounts(ctx context.Context, orgI
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) {
s.Calls.RetrieveServiceAccount = append(s.Calls.RetrieveServiceAccount, []interface{}{ctx, orgID, serviceAccountID})
return nil, nil
}

View File

@ -108,6 +108,11 @@ func (ss *SQLStore) GetOrgUsers(ctx context.Context, query *models.GetOrgUsersQu
whereConditions = append(whereConditions, "org_user.org_id = ?")
whereParams = append(whereParams, query.OrgId)
if query.UserID != 0 {
whereConditions = append(whereConditions, "org_user.user_id = ?")
whereParams = append(whereParams, query.UserID)
}
// 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", x.Dialect().Quote("user"), query.IsServiceAccount))