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-03-08 05:07:58 -06:00
|
|
|
"github.com/grafana/grafana/pkg/services/serviceaccounts/database"
|
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
|
|
|
}
|
|
|
|
|
|
|
|
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-03-15 04:48:10 -05: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))
|
2022-03-15 04:48:10 -05:00
|
|
|
serviceAccountsRoute.Delete("/:serviceAccountId", auth(middleware.ReqOrgAdmin,
|
|
|
|
accesscontrol.EvalPermission(serviceaccounts.ActionDelete, serviceaccounts.ScopeID)), routing.Wrap(api.DeleteServiceAccount))
|
2022-03-16 09:33:13 -05:00
|
|
|
// TODO:
|
|
|
|
// for 9.0 please reenable this with issue https://github.com/grafana/grafana-enterprise/issues/2969
|
|
|
|
// serviceAccountsRoute.Post("/upgradeall", auth(middleware.ReqOrgAdmin,
|
|
|
|
// accesscontrol.EvalPermission(serviceaccounts.ActionCreate)), routing.Wrap(api.UpgradeServiceAccounts))
|
|
|
|
// 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-03-08 05:07:58 -06:00
|
|
|
type createServiceAccountForm struct {
|
|
|
|
Name string `json:"name" binding:"Required"`
|
|
|
|
}
|
|
|
|
cmd := 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
|
|
|
|
2022-03-08 05:07:58 -06:00
|
|
|
serviceAccount, err := api.store.CreateServiceAccount(c.Req.Context(), c.OrgId, cmd.Name)
|
2021-12-14 07:39:25 -06:00
|
|
|
switch {
|
2022-03-08 05:07:58 -06:00
|
|
|
case errors.Is(err, &database.ErrSAInvalidName{}):
|
|
|
|
return response.Error(http.StatusBadRequest, "Invalid service account name", err)
|
2021-12-14 07:39:25 -06:00
|
|
|
case err != nil:
|
|
|
|
return response.Error(http.StatusInternalServerError, "Failed to create service account", err)
|
|
|
|
}
|
2022-03-08 05:07:58 -06:00
|
|
|
|
|
|
|
return response.JSON(http.StatusCreated, serviceAccount)
|
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-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{}
|
|
|
|
}
|
|
|
|
|
2022-03-21 11:58:18 -05:00
|
|
|
return accesscontrol.GetResourcesMetadata(c.Req.Context(), permissions, "serviceaccounts:id:", 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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-15 04:48:10 -05:00
|
|
|
saIDString := strconv.FormatInt(resp.Id, 10)
|
|
|
|
metadata := api.getAccessControlMetadata(c, map[string]bool{saIDString: true})
|
|
|
|
resp.AvatarUrl = dtos.GetGravatarUrlWithDefault("", resp.Name)
|
|
|
|
resp.AccessControl = metadata[saIDString]
|
|
|
|
|
2022-02-17 06:19:58 -06:00
|
|
|
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
|
|
|
|
}
|
2022-03-18 09:50:34 -05:00
|
|
|
// its okay that it fails, it is only filtering that might be weird, but to safe quard against any weird incoming query param
|
|
|
|
onlyWithExpiredTokens := c.QueryBool("expiredTokens")
|
|
|
|
filter := serviceaccounts.FilterIncludeAll
|
|
|
|
if onlyWithExpiredTokens {
|
|
|
|
filter = serviceaccounts.FilterOnlyExpiredTokens
|
|
|
|
}
|
|
|
|
serviceAccountSearch, err := api.store.SearchOrgServiceAccounts(ctx, c.OrgId, c.Query("query"), filter, page, perPage, c.SignedInUser)
|
2022-03-04 05:04:07 -06:00
|
|
|
if err != nil {
|
|
|
|
return response.Error(http.StatusInternalServerError, "Failed to get service accounts for current organization", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
saIDs := map[string]bool{}
|
2022-03-14 12:24:07 -05:00
|
|
|
for i := range serviceAccountSearch.ServiceAccounts {
|
|
|
|
sa := serviceAccountSearch.ServiceAccounts[i]
|
|
|
|
sa.AvatarUrl = dtos.GetGravatarUrlWithDefault("", sa.Name)
|
2022-03-04 05:04:07 -06:00
|
|
|
|
2022-03-14 12:24:07 -05:00
|
|
|
saIDString := strconv.FormatInt(sa.Id, 10)
|
2022-03-04 05:04:07 -06:00
|
|
|
saIDs[saIDString] = true
|
|
|
|
metadata := api.getAccessControlMetadata(c, map[string]bool{saIDString: true})
|
2022-03-14 12:24:07 -05:00
|
|
|
sa.AccessControl = metadata[strconv.FormatInt(sa.Id, 10)]
|
|
|
|
tokens, err := api.store.ListTokens(ctx, sa.OrgId, sa.Id)
|
2022-03-04 05:04:07 -06:00
|
|
|
if err != nil {
|
2022-03-14 12:24:07 -05:00
|
|
|
api.log.Warn("Failed to list tokens for service account", "serviceAccount", sa.Id)
|
2022-03-04 05:04:07 -06:00
|
|
|
}
|
2022-03-14 12:24:07 -05:00
|
|
|
sa.Tokens = int64(len(tokens))
|
2022-03-04 05:04:07 -06:00
|
|
|
}
|
|
|
|
|
2022-03-14 12:24:07 -05:00
|
|
|
return response.JSON(http.StatusOK, serviceAccountSearch)
|
2022-03-04 05:04:07 -06:00
|
|
|
}
|