internal/beater/auth/authenticator.go (102 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package auth
import (
"context"
"crypto/subtle"
"errors"
"fmt"
"time"
"github.com/elastic/apm-server/internal/beater/config"
"github.com/elastic/apm-server/internal/beater/headers"
"github.com/elastic/apm-server/internal/elasticsearch"
)
// Method identifies an authentication and authorization method.
type Method string
const (
// MethodNone is used when the server has no auth methods configured,
// meaning access is entirely unrestricted to unauthenticated clients.
//
// This exists to differentiate from allowed unauthenticated/anonymous
// access when the server has other auth methods defined.
MethodNone Method = "none"
// MethodAPIKey identifies the auth method using Elasticsearch API Keys.
// Clients that authenticate with an API Key may have restricted privileges.
MethodAPIKey Method = "api_key"
// MethodSecretToken identifies the auth methd using a shared secret token.
// Clients with this secret token have unrestricted privileges.
MethodSecretToken Method = "secret_token"
// MethodAnonymous identifies the anonymous access auth method.
// Anonymous clients will typically be restricted by agent and/or service.
MethodAnonymous Method = ""
)
// Action identifies an action to authorize.
type Action string
const (
// ActionAgentConfig is an Action describing an attempt to read agent config.
ActionAgentConfig Action = "agent_config"
// ActionEventIngest is an Action describing an attempt to ingest events.
ActionEventIngest Action = "event_ingest"
// ActionSourcemapUpload is an Action describing an attempt to upload a source map.
ActionSourcemapUpload Action = "sourcemap"
)
const (
cacheTimeoutMinute = 1 * time.Minute
expectedAuthHeaderFormat = "expected 'Authorization: Bearer secret_token' or 'Authorization: ApiKey base64(API key ID:API key)'"
)
// ErrAuthFailed is an error returned by Authenticator.Authenticate to indicate
// that a client has failed to authenticate, for example by failing to provide
// credentials or by providing an invalid or expired API Key.
var ErrAuthFailed = errors.New("authentication failed")
var errAuthMissing = fmt.Errorf("%w: missing or improperly formatted Authorization header: %s", ErrAuthFailed, expectedAuthHeaderFormat)
// ErrUnauthorized is an error returned by Authorizer.Authorize to indicate that
// the client is unauthorized for some action and resource. This should be wrapped
// to provide a reason, and checked using `errors.Is`.
var ErrUnauthorized = errors.New("unauthorized")
// Authenticator authenticates clients.
type Authenticator struct {
secretToken string
apikey *apikeyAuth
anonymous *anonymousAuth
}
// Authorizer provides an interface for authorizing an action and resource.
type Authorizer interface {
// Authorize checks if the client is authorized for the given action and
// resource, returning ErrUnauthorized if it is not. Other errors may be
// returned, for example because the server cannot communicate with
// external systems.
Authorize(context.Context, Action, Resource) error
}
// Resource holds parameters for restricting access that may be checked by
// Authorizer.Authorize.
type Resource struct {
// AgentName holds the agent name associated with the agent making the
// request. This may be empty if the agent is unknown or irrelevant,
// such as in a request to the healthcheck endpoint.
AgentName string
// ServiceName holds the service name associated with the agent making
// the request. This may be empty if the agent is unknown or irrelevant,
// such as in a request to the healthcheck endpoint.
ServiceName string
}
// AuthenticationDetails holds authentication details for a client.
type AuthenticationDetails struct {
// Method holds the authentication kind used.
//
// Method will be empty for unauthenticated (anonymous) requests when
// the server has at least one auth method defined. When the server
// has no auth methods defined, this will be MethodNone.
Method Method
// APIKey holds authentication details related to API Key auth.
// This will be set when Method is MethodAPIKey.
APIKey *APIKeyAuthenticationDetails
}
// APIKeyAuthenticationDetails holds API Key related authentication details.
type APIKeyAuthenticationDetails struct {
// ID holds the non-secret ID of the API Key.
ID string
// Username holds the username associated with the API Key.
Username string
}
// NewAuthenticator creates an Authenticator with config, authenticating
// clients with one of the allowed methods.
func NewAuthenticator(cfg config.AgentAuth) (*Authenticator, error) {
b := Authenticator{secretToken: cfg.SecretToken}
if cfg.APIKey.Enabled {
// Do not use apm-server's credentials for API Key requests;
// we should only use API Key credentials provided by clients
// to the Authenticate method.
cfg.APIKey.ESConfig.Username = ""
cfg.APIKey.ESConfig.Password = ""
cfg.APIKey.ESConfig.APIKey = ""
client, err := elasticsearch.NewClient(cfg.APIKey.ESConfig)
if err != nil {
return nil, err
}
cache, err := newPrivilegesCache(cacheTimeoutMinute, cfg.APIKey.LimitPerMin)
if err != nil {
return nil, err
}
b.apikey = newApikeyAuth(client, cache)
}
if cfg.Anonymous.Enabled {
b.anonymous = newAnonymousAuth(cfg.Anonymous.AllowAgent, cfg.Anonymous.AllowService)
}
return &b, nil
}
// Authenticate authenticates a client given an authentication method and token,
// returning the authentication details and an Authorizer for authorizing specific
// actions and resources.
//
// Authenticate will return ErrAuthFailed (possibly wrapped) if at least one auth
// method is configured and no valid credentials have been supplied. Other errors
// may be returned, for example because the server cannot communicate with external
// systems.
func (a *Authenticator) Authenticate(ctx context.Context, kind string, token string) (AuthenticationDetails, Authorizer, error) {
if a.apikey == nil && a.secretToken == "" {
// No auth required, let everyone through.
return AuthenticationDetails{Method: MethodNone}, allowAuth{}, nil
}
switch kind {
case "":
if a.anonymous != nil {
return AuthenticationDetails{Method: MethodAnonymous}, a.anonymous, nil
}
return AuthenticationDetails{}, nil, errAuthMissing
case headers.APIKey:
if a.apikey != nil {
details, authz, err := a.apikey.authenticate(ctx, token)
if err != nil {
return AuthenticationDetails{}, nil, err
}
return AuthenticationDetails{Method: MethodAPIKey, APIKey: details}, authz, nil
}
case headers.Bearer:
if a.secretToken != "" && subtle.ConstantTimeCompare([]byte(a.secretToken), []byte(token)) == 1 {
return AuthenticationDetails{Method: MethodSecretToken}, allowAuth{}, nil
}
default:
return AuthenticationDetails{}, nil, fmt.Errorf(
"%w: unknown Authentication header %s: %s",
ErrAuthFailed, kind, expectedAuthHeaderFormat,
)
}
return AuthenticationDetails{}, nil, ErrAuthFailed
}