The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/services/serviceaccounts/api/token.go

256 lines
8.3 KiB

package api
import (
"net/http"
"strconv"
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/components/satokengen"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/serviceaccounts"
"github.com/grafana/grafana/pkg/web"
)
const (
failedToDeleteMsg = "Failed to delete service account token"
ServiceID = "sa"
)
// swagger:model
type TokenDTO struct {
// example: 1
Id int64 `json:"id"`
// example: grafana
Name string `json:"name"`
// example: 2022-03-23T10:31:02Z
Created *time.Time `json:"created"`
// example: 2022-03-23T10:31:02Z
LastUsedAt *time.Time `json:"lastUsedAt"`
// example: 2022-03-23T10:31:02Z
Expiration *time.Time `json:"expiration"`
// example: 0
SecondsUntilExpiration *float64 `json:"secondsUntilExpiration"`
// example: false
HasExpired bool `json:"hasExpired"`
// example: false
IsRevoked *bool `json:"isRevoked"`
}
func hasExpired(expiration *int64) bool {
if expiration == nil {
return false
}
v := time.Unix(*expiration, 0)
return (v).Before(time.Now())
}
const sevenDaysAhead = 7 * 24 * time.Hour
// swagger:route GET /serviceaccounts/{serviceAccountId}/tokens service_accounts listTokens
//
// # Get service account tokens
//
// Required permissions (See note in the [introduction](https://grafana.com/docs/grafana/latest/developers/http_api/serviceaccount/#service-account-api) for an explanation):
// action: `serviceaccounts:read` scope: `global:serviceaccounts:id:1` (single service account)
//
// Requires basic authentication and that the authenticated user is a Grafana Admin.
//
// Responses:
// 200: listTokensResponse
// 400: badRequestError
// 401: unauthorisedError
// 403: forbiddenError
// 500: internalServerError
func (api *ServiceAccountsAPI) ListTokens(ctx *contextmodel.ReqContext) response.Response {
saID, err := strconv.ParseInt(web.Params(ctx.Req)[":serviceAccountId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err)
}
orgID := ctx.SignedInUser.GetOrgID()
saTokens, err := api.service.ListTokens(ctx.Req.Context(), &serviceaccounts.GetSATokensQuery{
OrgID: &orgID,
ServiceAccountID: &saID,
})
if err != nil {
return response.Error(http.StatusInternalServerError, "Internal server error", err)
}
result := make([]TokenDTO, len(saTokens))
for i, t := range saTokens {
var (
token = t // pin pointer
expiration *time.Time = nil
secondsUntilExpiration float64 = 0
)
isExpired := hasExpired(t.Expires)
if t.Expires != nil {
v := time.Unix(*t.Expires, 0)
expiration = &v
if !isExpired && (*expiration).Before(time.Now().Add(sevenDaysAhead)) {
secondsUntilExpiration = time.Until(*expiration).Seconds()
}
}
result[i] = TokenDTO{
Id: token.ID,
Name: token.Name,
Created: &token.Created,
Expiration: expiration,
SecondsUntilExpiration: &secondsUntilExpiration,
HasExpired: isExpired,
LastUsedAt: token.LastUsedAt,
IsRevoked: token.IsRevoked,
}
}
return response.JSON(http.StatusOK, result)
}
// swagger:route POST /serviceaccounts/{serviceAccountId}/tokens service_accounts createToken
//
// # CreateNewToken adds a token to a service account
//
// Required permissions (See note in the [introduction](https://grafana.com/docs/grafana/latest/developers/http_api/serviceaccount/#service-account-api) for an explanation):
// action: `serviceaccounts:write` scope: `serviceaccounts:id:1` (single service account)
//
// Responses:
// 200: createTokenResponse
// 400: badRequestError
// 401: unauthorisedError
// 403: forbiddenError
// 404: notFoundError
// 409: conflictError
// 500: internalServerError
func (api *ServiceAccountsAPI) CreateToken(c *contextmodel.ReqContext) response.Response {
saID, err := strconv.ParseInt(web.Params(c.Req)[":serviceAccountId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err)
}
// confirm service account exists
if _, err = api.service.RetrieveServiceAccount(c.Req.Context(), c.SignedInUser.GetOrgID(), saID); err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to retrieve service account", err)
}
cmd := serviceaccounts.AddServiceAccountTokenCommand{}
if err = web.Bind(c.Req, &cmd); err != nil {
return response.Error(http.StatusBadRequest, "Bad request data", err)
}
// Force affected service account to be the one referenced in the URL
cmd.OrgId = c.SignedInUser.GetOrgID()
if api.cfg.ApiKeyMaxSecondsToLive != -1 {
if cmd.SecondsToLive == 0 {
return response.Error(http.StatusBadRequest, "Number of seconds before expiration should be set", nil)
}
if cmd.SecondsToLive > api.cfg.ApiKeyMaxSecondsToLive {
return response.Error(http.StatusBadRequest, "Number of seconds before expiration is greater than the global limit", nil)
}
}
if api.cfg.SATokenExpirationDayLimit > 0 {
dayExpireLimit := time.Now().Add(time.Duration(api.cfg.SATokenExpirationDayLimit) * time.Hour * 24).Truncate(24 * time.Hour)
expirationDate := time.Now().Add(time.Duration(cmd.SecondsToLive) * time.Second).Truncate(24 * time.Hour)
if expirationDate.After(dayExpireLimit) {
return response.Respond(http.StatusBadRequest, "The expiration date input exceeds the limit for service account access tokens expiration date")
}
}
newKeyInfo, err := satokengen.New(ServiceID)
if err != nil {
return response.Error(http.StatusInternalServerError, "Generating service account token failed", err)
}
cmd.Key = newKeyInfo.HashedKey
apiKey, err := api.service.AddServiceAccountToken(c.Req.Context(), saID, &cmd)
if err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "failed to add service account token", err)
}
result := &dtos.NewApiKeyResult{
ID: apiKey.ID,
Name: apiKey.Name,
Key: newKeyInfo.ClientSecret,
}
return response.JSON(http.StatusOK, result)
}
// swagger:route DELETE /serviceaccounts/{serviceAccountId}/tokens/{tokenId} service_accounts deleteToken
//
// # DeleteToken deletes service account tokens
//
// Required permissions (See note in the [introduction](https://grafana.com/docs/grafana/latest/developers/http_api/serviceaccount/#service-account-api) for an explanation):
// action: `serviceaccounts:write` scope: `serviceaccounts:id:1` (single service account)
//
// Requires basic authentication and that the authenticated user is a Grafana Admin.
//
// Responses:
// 200: okResponse
// 400: badRequestError
// 401: unauthorisedError
// 403: forbiddenError
// 404: notFoundError
// 500: internalServerError
func (api *ServiceAccountsAPI) DeleteToken(c *contextmodel.ReqContext) response.Response {
saID, err := strconv.ParseInt(web.Params(c.Req)[":serviceAccountId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "Service Account ID is invalid", err)
}
// confirm service account exists
if _, err := api.service.RetrieveServiceAccount(c.Req.Context(), c.SignedInUser.GetOrgID(), saID); err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to retrieve service account", err)
}
tokenID, err := strconv.ParseInt(web.Params(c.Req)[":tokenId"], 10, 64)
if err != nil {
return response.Error(http.StatusBadRequest, "Token ID is invalid", err)
}
if err = api.service.DeleteServiceAccountToken(c.Req.Context(), c.SignedInUser.GetOrgID(), saID, tokenID); err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, failedToDeleteMsg, err)
}
return response.Success("Service account token deleted")
}
// swagger:parameters listTokens
type ListTokensParams struct {
// in:path
ServiceAccountId int64 `json:"serviceAccountId"`
}
// swagger:parameters createToken
type CreateTokenParams struct {
// in:path
ServiceAccountId int64 `json:"serviceAccountId"`
// in:body
Body serviceaccounts.AddServiceAccountTokenCommand
}
// swagger:parameters deleteToken
type DeleteTokenParams struct {
// in:path
TokenId int64 `json:"tokenId"`
// in:path
ServiceAccountId int64 `json:"serviceAccountId"`
}
// swagger:response listTokensResponse
type ListTokensResponse struct {
// in:body
Body *TokenDTO
}
// swagger:response createTokenResponse
type CreateTokenResponse struct {
// in:body
Body *dtos.NewApiKeyResult
}