2022-02-07 07:51:54 -06:00
package api
import (
"errors"
"net/http"
"strconv"
"time"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
2022-05-23 06:14:38 -05:00
apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed"
2022-02-07 07:51:54 -06:00
"github.com/grafana/grafana/pkg/models"
2022-08-04 07:19:09 -05:00
"github.com/grafana/grafana/pkg/services/apikey"
2022-02-07 07:51:54 -06:00
"github.com/grafana/grafana/pkg/services/serviceaccounts"
2022-06-16 09:02:03 -05:00
"github.com/grafana/grafana/pkg/services/serviceaccounts/database"
2022-02-07 07:51:54 -06:00
"github.com/grafana/grafana/pkg/web"
)
2022-05-23 06:14:38 -05:00
const (
2022-06-16 05:11:22 -05:00
failedToDeleteMsg = "Failed to delete service account token"
2022-05-23 06:14:38 -05:00
ServiceID = "sa"
)
2022-02-07 07:51:54 -06:00
2022-07-19 04:52:51 -05:00
// swagger:model
2022-02-18 04:43:33 -06:00
type TokenDTO struct {
2022-07-19 04:52:51 -05:00
// 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" `
2022-08-18 09:54:39 -05:00
// example: false
IsRevoked * bool ` json:"isRevoked" `
2022-02-18 04:43:33 -06:00
}
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
2022-07-27 08:54:37 -05:00
// swagger:route GET /serviceaccounts/{serviceAccountId}/tokens service_accounts listTokens
//
2022-08-18 09:54:39 -05:00
// # Get service account tokens
2022-07-27 08:54:37 -05:00
//
// 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
2022-02-07 07:51:54 -06:00
func ( api * ServiceAccountsAPI ) ListTokens ( ctx * models . ReqContext ) response . Response {
saID , 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-02-07 07:51:54 -06:00
}
2022-08-18 09:54:39 -05:00
saTokens , err := api . store . ListTokens ( ctx . Req . Context ( ) , & serviceaccounts . GetSATokensQuery {
OrgID : & ctx . OrgID ,
ServiceAccountID : & saID ,
} )
2022-06-16 05:11:22 -05:00
if err != nil {
return response . Error ( http . StatusInternalServerError , "Internal server error" , err )
}
2022-02-18 04:43:33 -06:00
2022-08-18 09:54:39 -05:00
result := make ( [ ] TokenDTO , len ( saTokens ) )
2022-06-16 05:11:22 -05:00
for i , t := range saTokens {
2022-08-18 09:54:39 -05:00
var (
token = t // pin pointer
expiration * time . Time = nil
secondsUntilExpiration float64 = 0
)
2022-06-16 05:11:22 -05:00
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 ( )
2022-02-07 07:51:54 -06:00
}
}
2022-08-18 09:54:39 -05:00
result [ i ] = TokenDTO {
Id : token . Id ,
Name : token . Name ,
Created : & token . Created ,
2022-06-16 05:11:22 -05:00
Expiration : expiration ,
SecondsUntilExpiration : & secondsUntilExpiration ,
HasExpired : isExpired ,
2022-08-18 09:54:39 -05:00
LastUsedAt : token . LastUsedAt ,
IsRevoked : token . IsRevoked ,
2022-06-16 05:11:22 -05:00
}
2022-02-07 07:51:54 -06:00
}
2022-06-16 05:11:22 -05:00
return response . JSON ( http . StatusOK , result )
2022-02-07 07:51:54 -06:00
}
2022-07-27 08:54:37 -05:00
// swagger:route POST /serviceaccounts/{serviceAccountId}/tokens service_accounts createToken
//
2022-08-18 09:54:39 -05:00
// # CreateNewToken adds a token to a service account
2022-07-27 08:54:37 -05:00
//
// 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
2022-02-07 07:51:54 -06:00
func ( api * ServiceAccountsAPI ) CreateToken ( c * models . ReqContext ) response . Response {
saID , 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-07 07:51:54 -06:00
}
// confirm service account exists
2022-08-11 06:28:55 -05:00
if _ , err := api . store . RetrieveServiceAccount ( c . Req . Context ( ) , c . OrgID , saID ) ; err != nil {
2022-02-07 07:51:54 -06:00
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-04-13 11:11:03 -05:00
cmd := serviceaccounts . AddServiceAccountTokenCommand { }
2022-02-07 07:51:54 -06:00
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-07 07:51:54 -06:00
}
// Force affected service account to be the one referenced in the URL
2022-08-11 06:28:55 -05:00
cmd . OrgId = c . OrgID
2022-02-07 07:51:54 -06:00
if api . cfg . ApiKeyMaxSecondsToLive != - 1 {
if cmd . SecondsToLive == 0 {
2022-02-17 08:00:56 -06:00
return response . Error ( http . StatusBadRequest , "Number of seconds before expiration should be set" , nil )
2022-02-07 07:51:54 -06:00
}
if cmd . SecondsToLive > api . cfg . ApiKeyMaxSecondsToLive {
2022-02-17 08:00:56 -06:00
return response . Error ( http . StatusBadRequest , "Number of seconds before expiration is greater than the global limit" , nil )
2022-02-07 07:51:54 -06:00
}
}
2022-05-23 06:14:38 -05:00
newKeyInfo , err := apikeygenprefix . New ( ServiceID )
2022-02-07 07:51:54 -06:00
if err != nil {
2022-06-16 05:11:22 -05:00
return response . Error ( http . StatusInternalServerError , "Generating service account token failed" , err )
2022-02-07 07:51:54 -06:00
}
cmd . Key = newKeyInfo . HashedKey
2022-02-28 04:30:45 -06:00
if err := api . store . AddServiceAccountToken ( c . Req . Context ( ) , saID , & cmd ) ; err != nil {
2022-06-16 09:02:03 -05:00
if errors . Is ( err , database . ErrInvalidTokenExpiration ) {
2022-02-17 08:00:56 -06:00
return response . Error ( http . StatusBadRequest , err . Error ( ) , nil )
2022-02-07 07:51:54 -06:00
}
2022-06-16 09:02:03 -05:00
if errors . Is ( err , database . ErrDuplicateToken ) {
2022-02-17 08:00:56 -06:00
return response . Error ( http . StatusConflict , err . Error ( ) , nil )
2022-02-07 07:51:54 -06:00
}
2022-06-16 05:11:22 -05:00
return response . Error ( http . StatusInternalServerError , "Failed to add service account token" , err )
2022-02-07 07:51:54 -06:00
}
result := & dtos . NewApiKeyResult {
ID : cmd . Result . Id ,
Name : cmd . Result . Name ,
Key : newKeyInfo . ClientSecret ,
}
2022-02-17 08:00:56 -06:00
return response . JSON ( http . StatusOK , result )
2022-02-07 07:51:54 -06:00
}
2022-07-27 08:54:37 -05:00
// swagger:route DELETE /serviceaccounts/{serviceAccountId}/tokens/{tokenId} service_accounts deleteToken
//
2022-08-18 09:54:39 -05:00
// # DeleteToken deletes service account tokens
2022-07-27 08:54:37 -05:00
//
// 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
2022-02-07 07:51:54 -06:00
func ( api * ServiceAccountsAPI ) DeleteToken ( c * models . ReqContext ) response . Response {
saID , err := strconv . ParseInt ( web . Params ( c . Req ) [ ":serviceAccountId" ] , 10 , 64 )
if err != nil {
2022-02-28 04:30:45 -06:00
return response . Error ( http . StatusBadRequest , "Service Account ID is invalid" , err )
2022-02-07 07:51:54 -06:00
}
// confirm service account exists
2022-08-11 06:28:55 -05:00
if _ , err := api . store . RetrieveServiceAccount ( c . Req . Context ( ) , c . OrgID , saID ) ; err != nil {
2022-02-07 07:51:54 -06:00
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 )
}
}
tokenID , err := strconv . ParseInt ( web . Params ( c . Req ) [ ":tokenId" ] , 10 , 64 )
if err != nil {
2022-02-28 04:30:45 -06:00
return response . Error ( http . StatusBadRequest , "Token ID is invalid" , err )
2022-02-07 07:51:54 -06:00
}
2022-08-11 06:28:55 -05:00
if err = api . store . DeleteServiceAccountToken ( c . Req . Context ( ) , c . OrgID , saID , tokenID ) ; err != nil {
2022-02-17 08:00:56 -06:00
status := http . StatusNotFound
2022-08-04 07:19:09 -05:00
if err != nil && ! errors . Is ( err , apikey . ErrNotFound ) {
2022-02-17 08:00:56 -06:00
status = http . StatusInternalServerError
2022-02-07 07:51:54 -06:00
} else {
2022-08-04 07:19:09 -05:00
err = apikey . ErrNotFound
2022-02-07 07:51:54 -06:00
}
return response . Error ( status , failedToDeleteMsg , err )
}
2022-06-16 05:11:22 -05:00
return response . Success ( "Service account token deleted" )
2022-02-07 07:51:54 -06:00
}
2022-07-27 08:54:37 -05:00
// 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
}