2021-11-11 09:10:24 -06:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
2021-12-14 07:39:25 -06:00
|
|
|
"errors"
|
2021-11-11 09:10:24 -06:00
|
|
|
"net/http"
|
2022-01-14 10:55:57 -06:00
|
|
|
"strconv"
|
2021-11-11 09:10:24 -06:00
|
|
|
|
2022-02-08 13:19:22 -06:00
|
|
|
"github.com/grafana/grafana/pkg/api/dtos"
|
2021-11-11 09:10:24 -06:00
|
|
|
"github.com/grafana/grafana/pkg/api/response"
|
|
|
|
"github.com/grafana/grafana/pkg/api/routing"
|
2022-02-08 13:19:22 -06:00
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
2021-11-11 09:10:24 -06:00
|
|
|
"github.com/grafana/grafana/pkg/middleware"
|
|
|
|
"github.com/grafana/grafana/pkg/models"
|
|
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
|
|
acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware"
|
2022-01-26 11:44:20 -06:00
|
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
2021-11-11 09:10:24 -06:00
|
|
|
"github.com/grafana/grafana/pkg/services/serviceaccounts"
|
2022-02-07 07:51:54 -06:00
|
|
|
"github.com/grafana/grafana/pkg/setting"
|
2021-12-14 07:39:25 -06:00
|
|
|
"github.com/grafana/grafana/pkg/web"
|
2021-11-11 09:10:24 -06:00
|
|
|
)
|
|
|
|
|
|
|
|
type ServiceAccountsAPI struct {
|
2022-02-07 07:51:54 -06:00
|
|
|
cfg *setting.Cfg
|
2021-11-11 09:10:24 -06:00
|
|
|
service serviceaccounts.Service
|
|
|
|
accesscontrol accesscontrol.AccessControl
|
|
|
|
RouterRegister routing.RouteRegister
|
2021-12-16 07:28:16 -06:00
|
|
|
store serviceaccounts.Store
|
2022-02-08 13:19:22 -06:00
|
|
|
log log.Logger
|
2021-11-11 09:10:24 -06:00
|
|
|
}
|
|
|
|
|
2022-02-08 07:31:34 -06:00
|
|
|
type serviceAccountIdDTO struct {
|
|
|
|
Id int64 `json:"id"`
|
|
|
|
Message string `json:"message"`
|
|
|
|
}
|
|
|
|
|
2021-11-11 09:10:24 -06:00
|
|
|
func NewServiceAccountsAPI(
|
2022-02-07 07:51:54 -06:00
|
|
|
cfg *setting.Cfg,
|
2021-11-11 09:10:24 -06:00
|
|
|
service serviceaccounts.Service,
|
|
|
|
accesscontrol accesscontrol.AccessControl,
|
|
|
|
routerRegister routing.RouteRegister,
|
2021-12-16 07:28:16 -06:00
|
|
|
store serviceaccounts.Store,
|
2021-11-11 09:10:24 -06:00
|
|
|
) *ServiceAccountsAPI {
|
|
|
|
return &ServiceAccountsAPI{
|
2022-02-07 07:51:54 -06:00
|
|
|
cfg: cfg,
|
2021-11-11 09:10:24 -06:00
|
|
|
service: service,
|
|
|
|
accesscontrol: accesscontrol,
|
|
|
|
RouterRegister: routerRegister,
|
2021-12-16 07:28:16 -06:00
|
|
|
store: store,
|
2022-02-08 13:19:22 -06:00
|
|
|
log: log.New("serviceaccounts.api"),
|
2021-11-11 09:10:24 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (api *ServiceAccountsAPI) RegisterAPIEndpoints(
|
2022-01-26 11:44:20 -06:00
|
|
|
features featuremgmt.FeatureToggles,
|
2021-11-11 09:10:24 -06:00
|
|
|
) {
|
2022-01-26 11:44:20 -06:00
|
|
|
if !features.IsEnabled(featuremgmt.FlagServiceAccounts) {
|
2021-11-11 09:10:24 -06:00
|
|
|
return
|
|
|
|
}
|
2022-01-19 10:03:45 -06:00
|
|
|
|
2021-11-11 09:10:24 -06:00
|
|
|
auth := acmiddleware.Middleware(api.accesscontrol)
|
2022-02-02 09:32:37 -06:00
|
|
|
api.RouterRegister.Group("/api/serviceaccounts", func(serviceAccountsRoute routing.RouteRegister) {
|
2022-01-12 06:23:00 -06:00
|
|
|
serviceAccountsRoute.Get("/", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionRead, serviceaccounts.ScopeAll)), routing.Wrap(api.ListServiceAccounts))
|
2022-03-04 05:04:07 -06:00
|
|
|
serviceAccountsRoute.Get("/search", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionRead)), routing.Wrap(api.SearchOrgServiceAccountsWithPaging))
|
2022-02-22 07:58:42 -06:00
|
|
|
serviceAccountsRoute.Post("/", auth(middleware.ReqOrgAdmin,
|
|
|
|
accesscontrol.EvalPermission(serviceaccounts.ActionCreate)), routing.Wrap(api.CreateServiceAccount))
|
|
|
|
serviceAccountsRoute.Get("/:serviceAccountId", auth(middleware.ReqOrgAdmin,
|
|
|
|
accesscontrol.EvalPermission(serviceaccounts.ActionRead, serviceaccounts.ScopeID)), routing.Wrap(api.RetrieveServiceAccount))
|
2022-02-17 06:19:58 -06:00
|
|
|
serviceAccountsRoute.Patch("/:serviceAccountId", auth(middleware.ReqOrgAdmin,
|
|
|
|
accesscontrol.EvalPermission(serviceaccounts.ActionWrite, serviceaccounts.ScopeID)), routing.Wrap(api.updateServiceAccount))
|
2021-11-11 09:10:24 -06:00
|
|
|
serviceAccountsRoute.Delete("/:serviceAccountId", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionDelete, serviceaccounts.ScopeID)), routing.Wrap(api.DeleteServiceAccount))
|
2022-02-08 06:44:27 -06:00
|
|
|
serviceAccountsRoute.Post("/upgradeall", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionCreate)), routing.Wrap(api.UpgradeServiceAccounts))
|
2022-01-20 09:51:18 -06:00
|
|
|
serviceAccountsRoute.Post("/convert/:keyId", auth(middleware.ReqOrgAdmin, accesscontrol.EvalPermission(serviceaccounts.ActionCreate, serviceaccounts.ScopeID)), routing.Wrap(api.ConvertToServiceAccount))
|
2022-02-07 07:51:54 -06:00
|
|
|
serviceAccountsRoute.Get("/:serviceAccountId/tokens", auth(middleware.ReqOrgAdmin,
|
|
|
|
accesscontrol.EvalPermission(serviceaccounts.ActionRead, serviceaccounts.ScopeID)), routing.Wrap(api.ListTokens))
|
|
|
|
serviceAccountsRoute.Post("/:serviceAccountId/tokens", auth(middleware.ReqOrgAdmin,
|
|
|
|
accesscontrol.EvalPermission(serviceaccounts.ActionWrite, serviceaccounts.ScopeID)), routing.Wrap(api.CreateToken))
|
|
|
|
serviceAccountsRoute.Delete("/:serviceAccountId/tokens/:tokenId", auth(middleware.ReqOrgAdmin,
|
|
|
|
accesscontrol.EvalPermission(serviceaccounts.ActionWrite, serviceaccounts.ScopeID)), routing.Wrap(api.DeleteToken))
|
2021-11-11 09:10:24 -06:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2021-12-14 07:39:25 -06:00
|
|
|
// POST /api/serviceaccounts
|
|
|
|
func (api *ServiceAccountsAPI) CreateServiceAccount(c *models.ReqContext) response.Response {
|
2022-02-17 06:19:58 -06:00
|
|
|
cmd := serviceaccounts.CreateServiceAccountForm{}
|
2021-12-14 07:39:25 -06:00
|
|
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
|
|
|
return response.Error(http.StatusBadRequest, "Bad request data", err)
|
|
|
|
}
|
2022-02-22 07:58:42 -06:00
|
|
|
cmd.OrgID = c.OrgId
|
|
|
|
|
2021-12-14 07:39:25 -06:00
|
|
|
user, err := api.service.CreateServiceAccount(c.Req.Context(), &cmd)
|
|
|
|
switch {
|
|
|
|
case errors.Is(err, serviceaccounts.ErrServiceAccountNotFound):
|
|
|
|
return response.Error(http.StatusBadRequest, "Failed to create role with the provided name", err)
|
|
|
|
case err != nil:
|
|
|
|
return response.Error(http.StatusInternalServerError, "Failed to create service account", err)
|
|
|
|
}
|
2022-02-08 07:31:34 -06:00
|
|
|
sa := &serviceAccountIdDTO{
|
2022-02-07 07:12:39 -06:00
|
|
|
Id: user.Id,
|
2022-02-08 07:31:34 -06:00
|
|
|
Message: "Service account created",
|
2022-02-07 07:12:39 -06:00
|
|
|
}
|
2022-02-08 07:31:34 -06:00
|
|
|
return response.JSON(http.StatusCreated, sa)
|
2021-12-14 07:39:25 -06:00
|
|
|
}
|
|
|
|
|
2021-11-11 09:10:24 -06:00
|
|
|
func (api *ServiceAccountsAPI) DeleteServiceAccount(ctx *models.ReqContext) response.Response {
|
2022-01-14 10:55:57 -06:00
|
|
|
scopeID, err := strconv.ParseInt(web.Params(ctx.Req)[":serviceAccountId"], 10, 64)
|
|
|
|
if err != nil {
|
|
|
|
return response.Error(http.StatusBadRequest, "serviceAccountId is invalid", err)
|
|
|
|
}
|
|
|
|
err = api.service.DeleteServiceAccount(ctx.Req.Context(), ctx.OrgId, scopeID)
|
2021-11-11 09:10:24 -06:00
|
|
|
if err != nil {
|
|
|
|
return response.Error(http.StatusInternalServerError, "Service account deletion error", err)
|
|
|
|
}
|
2022-03-01 02:21:55 -06:00
|
|
|
return response.Success("Service account deleted")
|
2021-11-11 09:10:24 -06:00
|
|
|
}
|
2021-12-16 07:28:16 -06:00
|
|
|
|
|
|
|
func (api *ServiceAccountsAPI) UpgradeServiceAccounts(ctx *models.ReqContext) response.Response {
|
|
|
|
if err := api.store.UpgradeServiceAccounts(ctx.Req.Context()); err == nil {
|
2022-03-01 02:21:55 -06:00
|
|
|
return response.Success("Service accounts upgraded")
|
2021-12-16 07:28:16 -06:00
|
|
|
} else {
|
2022-02-17 08:00:56 -06:00
|
|
|
return response.Error(http.StatusInternalServerError, "Internal server error", err)
|
2021-12-16 07:28:16 -06:00
|
|
|
}
|
|
|
|
}
|
2022-01-12 06:23:00 -06:00
|
|
|
|
2022-01-20 09:51:18 -06:00
|
|
|
func (api *ServiceAccountsAPI) ConvertToServiceAccount(ctx *models.ReqContext) response.Response {
|
|
|
|
keyId, err := strconv.ParseInt(web.Params(ctx.Req)[":keyId"], 10, 64)
|
|
|
|
if err != nil {
|
2022-03-01 02:21:55 -06:00
|
|
|
return response.Error(http.StatusBadRequest, "Key ID is invalid", err)
|
2022-01-20 09:51:18 -06:00
|
|
|
}
|
|
|
|
if err := api.store.ConvertToServiceAccounts(ctx.Req.Context(), []int64{keyId}); err == nil {
|
2022-03-01 02:21:55 -06:00
|
|
|
return response.Success("Service accounts converted")
|
2022-01-20 09:51:18 -06:00
|
|
|
} else {
|
|
|
|
return response.Error(500, "Internal server error", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-08 13:19:22 -06:00
|
|
|
func (api *ServiceAccountsAPI) ListServiceAccounts(c *models.ReqContext) response.Response {
|
|
|
|
serviceAccounts, err := api.store.ListServiceAccounts(c.Req.Context(), c.OrgId, -1)
|
2022-01-12 06:23:00 -06:00
|
|
|
if err != nil {
|
2022-01-19 03:23:46 -06:00
|
|
|
return response.Error(http.StatusInternalServerError, "Failed to list service accounts", err)
|
2022-01-12 06:23:00 -06:00
|
|
|
}
|
2022-02-08 13:19:22 -06:00
|
|
|
|
|
|
|
saIDs := map[string]bool{}
|
|
|
|
for i := range serviceAccounts {
|
|
|
|
serviceAccounts[i].AvatarUrl = dtos.GetGravatarUrlWithDefault("", serviceAccounts[i].Name)
|
|
|
|
saIDs[strconv.FormatInt(serviceAccounts[i].Id, 10)] = true
|
|
|
|
}
|
|
|
|
|
2022-02-18 04:27:00 -06:00
|
|
|
metadata := api.getAccessControlMetadata(c, saIDs)
|
|
|
|
if len(metadata) > 0 {
|
2022-02-08 13:19:22 -06:00
|
|
|
for i := range serviceAccounts {
|
|
|
|
serviceAccounts[i].AccessControl = metadata[strconv.FormatInt(serviceAccounts[i].Id, 10)]
|
|
|
|
}
|
|
|
|
}
|
2022-01-12 06:23:00 -06:00
|
|
|
return response.JSON(http.StatusOK, serviceAccounts)
|
|
|
|
}
|
2022-01-19 03:23:46 -06:00
|
|
|
|
2022-02-18 04:27:00 -06:00
|
|
|
func (api *ServiceAccountsAPI) getAccessControlMetadata(c *models.ReqContext, saIDs map[string]bool) map[string]accesscontrol.Metadata {
|
2022-02-08 13:19:22 -06:00
|
|
|
if api.accesscontrol.IsDisabled() || !c.QueryBool("accesscontrol") {
|
2022-02-18 04:27:00 -06:00
|
|
|
return map[string]accesscontrol.Metadata{}
|
2022-02-08 13:19:22 -06:00
|
|
|
}
|
|
|
|
|
2022-02-18 04:27:00 -06:00
|
|
|
if c.SignedInUser.Permissions == nil {
|
|
|
|
return map[string]accesscontrol.Metadata{}
|
2022-02-08 13:19:22 -06:00
|
|
|
}
|
|
|
|
|
2022-02-18 04:27:00 -06:00
|
|
|
permissions, ok := c.SignedInUser.Permissions[c.OrgId]
|
|
|
|
if !ok {
|
|
|
|
return map[string]accesscontrol.Metadata{}
|
|
|
|
}
|
|
|
|
|
|
|
|
return accesscontrol.GetResourcesMetadata(c.Req.Context(), permissions, "serviceaccounts", saIDs)
|
2022-02-08 13:19:22 -06:00
|
|
|
}
|
|
|
|
|
2022-01-19 03:23:46 -06:00
|
|
|
func (api *ServiceAccountsAPI) RetrieveServiceAccount(ctx *models.ReqContext) response.Response {
|
|
|
|
scopeID, err := strconv.ParseInt(web.Params(ctx.Req)[":serviceAccountId"], 10, 64)
|
|
|
|
if err != nil {
|
2022-03-01 02:21:55 -06:00
|
|
|
return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err)
|
2022-01-19 03:23:46 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
2022-02-22 07:58:42 -06:00
|
|
|
|
2022-02-25 04:33:34 -06:00
|
|
|
saIDString := strconv.FormatInt(serviceAccount.Id, 10)
|
|
|
|
metadata := api.getAccessControlMetadata(ctx, map[string]bool{saIDString: true})
|
2022-02-22 07:58:42 -06:00
|
|
|
serviceAccount.AvatarUrl = dtos.GetGravatarUrlWithDefault("", serviceAccount.Name)
|
2022-02-25 04:33:34 -06:00
|
|
|
serviceAccount.AccessControl = metadata[saIDString]
|
2022-01-19 03:23:46 -06:00
|
|
|
return response.JSON(http.StatusOK, serviceAccount)
|
|
|
|
}
|
2022-02-17 06:19:58 -06:00
|
|
|
|
|
|
|
func (api *ServiceAccountsAPI) updateServiceAccount(c *models.ReqContext) response.Response {
|
|
|
|
scopeID, err := strconv.ParseInt(web.Params(c.Req)[":serviceAccountId"], 10, 64)
|
|
|
|
if err != nil {
|
2022-03-01 02:21:55 -06:00
|
|
|
return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err)
|
2022-02-17 06:19:58 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
cmd := &serviceaccounts.UpdateServiceAccountForm{}
|
|
|
|
if err := web.Bind(c.Req, &cmd); err != nil {
|
2022-03-01 02:21:55 -06:00
|
|
|
return response.Error(http.StatusBadRequest, "Bad request data", err)
|
2022-02-17 06:19:58 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
if cmd.Role != nil && !cmd.Role.IsValid() {
|
|
|
|
return response.Error(http.StatusBadRequest, "Invalid role specified", nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp, err := api.store.UpdateServiceAccount(c.Req.Context(), c.OrgId, scopeID, cmd)
|
|
|
|
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 update service account", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return response.JSON(http.StatusOK, resp)
|
|
|
|
}
|
2022-03-04 05:04:07 -06:00
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|