mirror of
https://github.com/grafana/grafana.git
synced 2025-07-29 19:32:51 +08:00

* Add tracing to auth clients + authtoken svc * Fix span names * Fix ext_jwt.go * Fix idimpl/service * Update wire_gen.go * Add tracing to JWT client * Lint
246 lines
6.5 KiB
Go
246 lines
6.5 KiB
Go
package clients
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"go.opentelemetry.io/otel/trace"
|
|
|
|
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/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"))
|
|
)
|
|
|
|
var (
|
|
_ authn.HookClient = new(APIKey)
|
|
_ authn.ContextAwareClient = new(APIKey)
|
|
)
|
|
|
|
const (
|
|
metaKeyID = "keyID"
|
|
metaKeySkipLastUsed = "keySkipLastUsed"
|
|
)
|
|
|
|
func ProvideAPIKey(apiKeyService apikey.Service, tracer trace.Tracer) *APIKey {
|
|
return &APIKey{
|
|
log: log.New(authn.ClientAPIKey),
|
|
apiKeyService: apiKeyService,
|
|
tracer: tracer,
|
|
}
|
|
}
|
|
|
|
type APIKey struct {
|
|
log log.Logger
|
|
apiKeyService apikey.Service
|
|
tracer trace.Tracer
|
|
}
|
|
|
|
func (s *APIKey) Name() string {
|
|
return authn.ClientAPIKey
|
|
}
|
|
|
|
func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
|
ctx, span := s.tracer.Start(ctx, "authn.apikey.Authenticate")
|
|
defer span.End()
|
|
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")
|
|
}
|
|
|
|
return newServiceAccountIdentity(key), nil
|
|
}
|
|
|
|
func (s *APIKey) IsEnabled() bool {
|
|
return true
|
|
}
|
|
|
|
func (s *APIKey) getAPIKey(ctx context.Context, token string) (*apikey.APIKey, error) {
|
|
ctx, span := s.tracer.Start(ctx, "authn.apikey.getAPIKey")
|
|
defer span.End()
|
|
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) {
|
|
ctx, span := s.tracer.Start(ctx, "authn.apikey.getFromToken")
|
|
defer span.End()
|
|
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) {
|
|
ctx, span := s.tracer.Start(ctx, "authn.apikey.getFromTokenLegacy")
|
|
defer span.End()
|
|
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) Hook(ctx context.Context, identity *authn.Identity, r *authn.Request) error {
|
|
ctx, span := s.tracer.Start(ctx, "authn.apikey.Hook") //nolint:ineffassign,staticcheck
|
|
defer span.End()
|
|
|
|
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")
|
|
}
|
|
|
|
// plain API keys are no longer supported so an error is returned if the api key doesn't belong to a service account
|
|
if key.ServiceAccountId == nil || *key.ServiceAccountId < 1 {
|
|
return errAPIKeyInvalid.Errorf("API key does not belong to a service account")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|