internal/pkg/api/auth.go (135 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.
// Package api exposes fleet-server's API to agents.
package api
import (
"errors"
"net/http"
"time"
"github.com/elastic/fleet-server/v7/internal/pkg/apikey"
"github.com/elastic/fleet-server/v7/internal/pkg/bulk"
"github.com/elastic/fleet-server/v7/internal/pkg/cache"
"github.com/elastic/fleet-server/v7/internal/pkg/model"
"github.com/rs/zerolog/hlog"
"go.elastic.co/apm/v2"
)
var (
ErrAPIKeyNotEnabled = errors.New("APIKey not enabled")
ErrAgentCorrupted = errors.New("agent record corrupted")
ErrAgentInactive = errors.New("agent inactive")
ErrAgentIdentity = errors.New("agent header contains wrong identifier")
)
// authAPIKey authenticates the provided API key, it checks that the key exists and is enabled.
// WARNING: This does not validate that the api key is valid for the Fleet Domain.
// An additional check must be executed to validate it is not a random api key.
func authAPIKey(r *http.Request, bulker bulk.Bulk, c cache.Cache) (*apikey.APIKey, error) {
span, ctx := apm.StartSpan(r.Context(), "authKey", "auth")
defer span.End()
start := time.Now()
key, err := apikey.ExtractAPIKey(r)
if err != nil {
return nil, err
}
if c.ValidAPIKey(*key) {
span.Context.SetLabel("api_key_cache_hit", true)
hlog.FromRequest(r).Debug().
Str("id", key.ID).
Int64(ECSEventDuration, time.Since(start).Nanoseconds()).
Bool("fleet.apikey.cache_hit", true).
Msg("ApiKey authenticated")
return key, nil
} else {
span.Context.SetLabel("api_key_cache_hit", false)
}
info, err := bulker.APIKeyAuth(ctx, *key)
if err != nil {
hlog.FromRequest(r).Info().
Err(err).
Str(LogAPIKeyID, key.ID).
Int64(ECSEventDuration, time.Since(start).Nanoseconds()).
Msg("ApiKey fail authentication")
return nil, err
}
hlog.FromRequest(r).Debug().
Str("id", key.ID).
Int64(ECSEventDuration, time.Since(start).Nanoseconds()).
Str("userName", info.UserName).
Strs("roles", info.Roles).
Bool("enabled", info.Enabled).
RawJSON("meta", info.Metadata).
Bool("fleet.apikey.cache_hit", false).
Msg("ApiKey authenticated")
c.SetAPIKey(*key, info.Enabled)
if !info.Enabled {
err = ErrAPIKeyNotEnabled
hlog.FromRequest(r).Info().
Err(err).
Str("id", key.ID).
Int64(ECSEventDuration, time.Since(start).Nanoseconds()).
Msg("ApiKey not enabled")
}
return key, err
}
// authAgent ensures that the requested API-Key is associated with the correct agent.
// If all succeeds, it returns the agent associated with id.
func authAgent(r *http.Request, id *string, bulker bulk.Bulk, c cache.Cache) (*model.Agent, error) {
span, ctx := apm.StartSpan(r.Context(), "authAgent", "auth")
defer span.End()
r = r.WithContext(ctx)
start := time.Now()
// authenticate
key, err := authAPIKey(r, bulker, c)
if err != nil {
return nil, err
}
w := hlog.FromRequest(r).With().
Str(LogAccessAPIKeyID, key.ID)
if id != nil {
w = w.Str(LogAgentID, *id)
}
zlog := w.Logger()
authTime := time.Now()
if authTime.Sub(start) > time.Second {
zlog.Debug().
Int64(ECSEventDuration, authTime.Sub(start).Nanoseconds()).
Msg("authApiKey slow")
}
var agent *model.Agent
// If we have the agentID retrieve the agent document with a get (more performant) instead of triggering a search
if id != nil {
agent, err = getAgentAndVerifyAPIKeyID(ctx, bulker, *id, key.ID)
} else {
agent, err = findAgentByAPIKeyID(ctx, bulker, key.ID)
}
if err != nil {
return nil, err
}
tx := apm.TransactionFromContext(ctx)
if tx != nil {
tx.Context.SetLabel("agent_id", agent.Id)
}
if agent.Agent == nil {
zlog.Warn().
Err(ErrAgentCorrupted).
Msg("agent record does not contain required metadata section")
return nil, ErrAgentCorrupted
}
findTime := time.Now()
if findTime.Sub(authTime) > time.Second {
zlog.Debug().
Int64(ECSEventDuration, findTime.Sub(authTime).Nanoseconds()).
Msg("findAgentByApiKeyId slow")
}
// validate that the Access ApiKey identifier stored in the agent's record
// is in alignment when the authenticated key provided on this transaction
if agent.AccessAPIKeyID != key.ID {
zlog.Warn().
Err(ErrAgentCorrupted).
Str("agent.AccessApiKeyId", agent.AccessAPIKeyID).
Msg("agent access ApiKey id mismatch agent record")
return nil, ErrAgentCorrupted
}
// validate that the id in the header is equal to the agent id record
if id != nil && *id != agent.Id {
zlog.Warn().
Err(ErrAgentIdentity).
Str("agent.Id", agent.Id).
Msg("agent id mismatch against http header")
return nil, ErrAgentIdentity
}
// validate active, an api key can be valid for an inactive agent record
// if it is in our cache and has not timed out.
if !agent.Active {
zlog.Info().
Err(ErrAgentInactive).
Msg("agent record inactive")
// Update the cache to mark the api key id associated with this agent as not enabled
c.SetAPIKey(*key, false)
return agent, ErrAgentInactive
}
return agent, nil
}