mirror of
https://github.com/grafana/grafana.git
synced 2025-02-11 16:15:42 -06:00
* API: Add reqSignedIn to router groups * AuthN: Add fall through in context handler * AuthN: Add IsAnonymous field * AuthN: add priority to context aware clients * ContextHandler: Add comment * AuthN: Add a simple priority queue * AuthN: Add Name to client interface * AuthN: register clients with function * AuthN: update mock and fake to implement interface * AuthN: rewrite test without reflection * AuthN: add comment * AuthN: fix queue insert * AuthN: rewrite tests * AuthN: make the queue generic so we can reuse it for hooks * ContextHandler: Add fixme for auth headers * AuthN: remove unused variable * AuthN: use multierror * AuthN: write proper tests for queue * AuthN: Add queue item that can store the value and priority Co-authored-by: Jo <joao.guerreiro@grafana.com>
167 lines
4.4 KiB
Go
167 lines
4.4 KiB
Go
package clients
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana/pkg/components/apikeygen"
|
|
apikeygenprefix "github.com/grafana/grafana/pkg/components/apikeygenprefixed"
|
|
"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/org"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
"github.com/grafana/grafana/pkg/util/errutil"
|
|
)
|
|
|
|
var (
|
|
errAPIKeyInvalid = errutil.NewBase(errutil.StatusUnauthorized, "api-key.invalid", errutil.WithPublicMessage("Invalid API key"))
|
|
errAPIKeyExpired = errutil.NewBase(errutil.StatusUnauthorized, "api-key.expired", errutil.WithPublicMessage("Expired API key"))
|
|
errAPIKeyRevoked = errutil.NewBase(errutil.StatusUnauthorized, "api-key.revoked", errutil.WithPublicMessage("Revoked API key"))
|
|
)
|
|
|
|
var _ authn.ContextAwareClient = new(APIKey)
|
|
|
|
func ProvideAPIKey(apiKeyService apikey.Service, userService user.Service) *APIKey {
|
|
return &APIKey{
|
|
log: log.New(authn.ClientAPIKey),
|
|
userService: userService,
|
|
apiKeyService: apiKeyService,
|
|
}
|
|
}
|
|
|
|
type APIKey struct {
|
|
log log.Logger
|
|
userService user.Service
|
|
apiKeyService apikey.Service
|
|
}
|
|
|
|
func (s *APIKey) Name() string {
|
|
return authn.ClientAPIKey
|
|
}
|
|
|
|
func (s *APIKey) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
|
|
apiKey, 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 apiKey.Expires != nil && *apiKey.Expires <= time.Now().Unix() {
|
|
return nil, errAPIKeyExpired.Errorf("API key has expired")
|
|
}
|
|
|
|
if apiKey.IsRevoked != nil && *apiKey.IsRevoked {
|
|
return nil, errAPIKeyRevoked.Errorf("Api key is revoked")
|
|
}
|
|
|
|
// if the api key don't belong to a service account construct the identity and return it
|
|
if apiKey.ServiceAccountId == nil || *apiKey.ServiceAccountId < 1 {
|
|
return &authn.Identity{
|
|
ID: authn.NamespacedID(authn.NamespaceAPIKey, apiKey.Id),
|
|
OrgID: apiKey.OrgId,
|
|
OrgRoles: map[int64]org.RoleType{apiKey.OrgId: apiKey.Role},
|
|
}, nil
|
|
}
|
|
|
|
usr, err := s.userService.GetSignedInUserWithCacheCtx(ctx, &user.GetSignedInUserQuery{
|
|
UserID: *apiKey.ServiceAccountId,
|
|
OrgID: apiKey.OrgId,
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return authn.IdentityFromSignedInUser(authn.NamespacedID(authn.NamespaceServiceAccount, usr.UserID), usr, authn.ClientParams{}), nil
|
|
}
|
|
|
|
func (s *APIKey) getAPIKey(ctx context.Context, token string) (*apikey.APIKey, error) {
|
|
fn := s.getFromToken
|
|
if !strings.HasPrefix(token, apikeygenprefix.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 := apikeygenprefix.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}
|
|
if err := s.apiKeyService.GetApiKeyByName(ctx, &keyQuery); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// validate api key
|
|
isValid, err := apikeygen.IsValid(decoded, keyQuery.Result.Key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !isValid {
|
|
return nil, apikeygen.ErrInvalidApiKey
|
|
}
|
|
|
|
return keyQuery.Result, nil
|
|
}
|
|
|
|
func (s *APIKey) Test(ctx context.Context, r *authn.Request) bool {
|
|
return looksLikeApiKey(getTokenFromRequest(r))
|
|
}
|
|
|
|
func (s *APIKey) Priority() uint {
|
|
return 30
|
|
}
|
|
|
|
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 ""
|
|
}
|