2025-04-01 10:38:02 +09:00

278 lines
7.2 KiB
Go

package clients
import (
"context"
"errors"
"strconv"
"strings"
"time"
claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/components/apikeygen"
"github.com/grafana/grafana/pkg/components/satokengen"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/util"
)
var (
errAPIKeyInvalid = errutil.Unauthorized("api-key.invalid", errutil.WithPublicMessage("Invalid API key"))
errAPIKeyExpired = errutil.Unauthorized("api-key.expired", errutil.WithPublicMessage("Expired API key"))
errAPIKeyRevoked = errutil.Unauthorized("api-key.revoked", errutil.WithPublicMessage("Revoked API key"))
errAPIKeyOrgMismatch = errutil.Unauthorized("api-key.organization-mismatch", errutil.WithPublicMessage("API key does not belong to the requested organization"))
errAPIKeyInvalidType = errutil.BadRequest("api-key.invalid-type-id")
)
var (
_ authn.HookClient = new(APIKey)
_ authn.ContextAwareClient = new(APIKey)
_ authn.IdentityResolverClient = new(APIKey)
)
const (
metaKeyID = "keyID"
metaKeySkipLastUsed = "keySkipLastUsed"
)
func ProvideAPIKey(apiKeyService apikey.Service) *APIKey {
return &APIKey{
log: log.New(authn.ClientAPIKey),
apiKeyService: apiKeyService,
}
}
type APIKey struct {
log log.Logger
apiKeyService apikey.Service
}
func (s *APIKey) Name() string {
return authn.ClientAPIKey
}
func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
key, err := s.getAPIKey(ctx, getTokenFromRequest(r))
if err != nil {
if errors.Is(err, apikeygen.ErrInvalidApiKey) {
return nil, errAPIKeyInvalid.Errorf("API key is invalid")
}
return nil, err
}
if r.OrgID == 0 {
r.OrgID = key.OrgID
}
if err := validateApiKey(r.OrgID, key); err != nil {
return nil, err
}
// Set keyID so we can use it in last used hook
r.SetMeta(metaKeyID, strconv.FormatInt(key.ID, 10))
if !shouldUpdateLastUsedAt(key) {
// Hack to just have some value, we will check this key in the hook
// and if its not an empty string we will not update last used.
r.SetMeta(metaKeySkipLastUsed, "true")
}
// if the api key don't belong to a service account construct the identity and return it
if key.ServiceAccountId == nil || *key.ServiceAccountId < 1 {
return newAPIKeyIdentity(key), nil
}
return newServiceAccountIdentity(key), nil
}
func (s *APIKey) IsEnabled() bool {
return true
}
func (s *APIKey) getAPIKey(ctx context.Context, token string) (*apikey.APIKey, error) {
fn := s.getFromToken
if !strings.HasPrefix(token, satokengen.GrafanaPrefix) {
fn = s.getFromTokenLegacy
}
apiKey, err := fn(ctx, token)
if err != nil {
return nil, err
}
return apiKey, nil
}
func (s *APIKey) getFromToken(ctx context.Context, token string) (*apikey.APIKey, error) {
decoded, err := satokengen.Decode(token)
if err != nil {
return nil, err
}
hash, err := decoded.Hash()
if err != nil {
return nil, err
}
return s.apiKeyService.GetAPIKeyByHash(ctx, hash)
}
func (s *APIKey) getFromTokenLegacy(ctx context.Context, token string) (*apikey.APIKey, error) {
decoded, err := apikeygen.Decode(token)
if err != nil {
return nil, err
}
// fetch key
keyQuery := apikey.GetByNameQuery{KeyName: decoded.Name, OrgID: decoded.OrgId}
key, err := s.apiKeyService.GetApiKeyByName(ctx, &keyQuery)
if err != nil {
return nil, err
}
// validate api key
isValid, err := apikeygen.IsValid(decoded, key.Key)
if err != nil {
return nil, err
}
if !isValid {
return nil, apikeygen.ErrInvalidApiKey
}
return key, nil
}
func (s *APIKey) Test(ctx context.Context, r *authn.Request) bool {
return looksLikeApiKey(getTokenFromRequest(r))
}
func (s *APIKey) Priority() uint {
return 30
}
func (s *APIKey) IdentityType() claims.IdentityType {
return claims.TypeAPIKey
}
func (s *APIKey) ResolveIdentity(ctx context.Context, orgID int64, typ claims.IdentityType, id string) (*authn.Identity, error) {
if !claims.IsIdentityType(typ, claims.TypeAPIKey) {
return nil, errAPIKeyInvalidType.Errorf("got unexpected type: %s", typ)
}
apiKeyID, err := strconv.ParseInt(id, 10, 64)
if err != nil {
return nil, err
}
key, err := s.apiKeyService.GetApiKeyById(ctx, &apikey.GetByIDQuery{
ApiKeyID: apiKeyID,
})
if err != nil {
return nil, err
}
if err := validateApiKey(orgID, key); err != nil {
return nil, err
}
if key.ServiceAccountId != nil && *key.ServiceAccountId >= 1 {
return nil, errAPIKeyInvalidType.Errorf("api key belongs to service account")
}
return newAPIKeyIdentity(key), nil
}
func (s *APIKey) Hook(ctx context.Context, identity *authn.Identity, r *authn.Request) error {
if r.GetMeta(metaKeySkipLastUsed) != "" {
return nil
}
go func(keyID string) {
defer func() {
if err := recover(); err != nil {
s.log.Error("Panic during user last seen sync", "err", err)
}
}()
id, err := strconv.ParseInt(keyID, 10, 64)
if err != nil {
s.log.Warn("Invalid api key id", "id", keyID, "err", err)
return
}
if err := s.apiKeyService.UpdateAPIKeyLastUsedDate(context.Background(), id); err != nil {
s.log.Warn("Failed to update last used date for api key", "id", keyID, "err", err)
return
}
}(r.GetMeta(metaKeyID))
return nil
}
func looksLikeApiKey(token string) bool {
return token != ""
}
func getTokenFromRequest(r *authn.Request) string {
// api keys are only supported through http requests
if r.HTTPRequest == nil {
return ""
}
header := r.HTTPRequest.Header.Get("Authorization")
if strings.HasPrefix(header, bearerPrefix) {
return strings.TrimPrefix(header, bearerPrefix)
}
if strings.HasPrefix(header, basicPrefix) {
username, password, err := util.DecodeBasicAuthHeader(header)
if err == nil && username == "api_key" {
return password
}
}
return ""
}
func validateApiKey(orgID int64, key *apikey.APIKey) error {
if key.Expires != nil && *key.Expires <= time.Now().Unix() {
return errAPIKeyExpired.Errorf("API key has expired")
}
if key.IsRevoked != nil && *key.IsRevoked {
return errAPIKeyRevoked.Errorf("Api key is revoked")
}
if orgID != key.OrgID {
return errAPIKeyOrgMismatch.Errorf("API does not belong in Organization")
}
return nil
}
func newAPIKeyIdentity(key *apikey.APIKey) *authn.Identity {
return &authn.Identity{
ID: strconv.FormatInt(key.ID, 10),
Type: claims.TypeAPIKey,
OrgID: key.OrgID,
OrgRoles: map[int64]org.RoleType{key.OrgID: key.Role},
ClientParams: authn.ClientParams{SyncPermissions: true},
AuthenticatedBy: login.APIKeyAuthModule,
}
}
func newServiceAccountIdentity(key *apikey.APIKey) *authn.Identity {
return &authn.Identity{
ID: strconv.FormatInt(*key.ServiceAccountId, 10),
Type: claims.TypeServiceAccount,
OrgID: key.OrgID,
AuthenticatedBy: login.APIKeyAuthModule,
ClientParams: authn.ClientParams{FetchSyncedUser: true, SyncPermissions: true},
}
}
func shouldUpdateLastUsedAt(key *apikey.APIKey) bool {
return key.LastUsedAt == nil || time.Since(*key.LastUsedAt) > 5*time.Minute
}