Skip to content

Commit

Permalink
authorization: introduce anonymous + agent/service-specific auth (#5422)
Browse files Browse the repository at this point in the history
* beater/authorization: introduce Resource type

Introduce a Resource type, which describes a specific
resource for which authorization is being queried. This
can later be used to restrict access to specific agents
and services.

If the supplied resource is the zero value, then the
query is interpreted as checking if the requester has
any access at all. If the resource is non-zero, then
the query is interpreted as checking if the requester
has access to that specific resource (agent/service).

* beater/authorization: add context functions

* beater/authorization: introduce AnonymousAuth

* beater: check authorization for agent+service

(cherry picked from commit 05cde22)
  • Loading branch information
axw authored and mergify-bot committed Jun 10, 2021
1 parent 9588023 commit 57732ac
Show file tree
Hide file tree
Showing 26 changed files with 498 additions and 189 deletions.
68 changes: 48 additions & 20 deletions beater/api/config/agent/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/elastic/beats/v7/libbeat/monitoring"

"github.com/elastic/apm-server/agentcfg"
"github.com/elastic/apm-server/beater/authorization"
"github.com/elastic/apm-server/beater/config"
"github.com/elastic/apm-server/beater/headers"
"github.com/elastic/apm-server/beater/request"
Expand Down Expand Up @@ -91,21 +92,43 @@ func (h *handler) Handle(c *request.Context) {

query, queryErr := buildQuery(c)
if queryErr != nil {
extractQueryError(c, queryErr, c.AuthResult.Authorized)
extractQueryError(c, queryErr)
c.Write()
return
}
if query.Service.Environment == "" {
query.Service.Environment = h.defaultServiceEnvironment
}

if !c.AuthResult.Anonymous {
// The exact agent is not always known for anonymous clients, so we do not
// issue a secondary authorization check for them. Instead, we issue the
// request and filter the results using query.InsecureAgents.
authResource := authorization.Resource{ServiceName: query.Service.Name}
if result, err := authorization.AuthorizedFor(c.Request.Context(), authResource); err != nil {
c.Result.SetDefault(request.IDResponseErrorsServiceUnavailable)
c.Result.Err = err
c.Write()
return
} else if !result.Authorized {
id := request.IDResponseErrorsUnauthorized
status := request.MapResultIDToStatus[id]
if result.Reason != "" {
status.Keyword = result.Reason
}
c.Result.Set(id, status.Code, status.Keyword, nil, nil)
c.Write()
return
}
}

result, err := h.f.Fetch(c.Request.Context(), query)
if err != nil {
var verr *agentcfg.ValidationError
if errors.As(err, &verr) {
body := verr.Body()
if strings.HasPrefix(body, agentcfg.ErrMsgKibanaVersionNotCompatible) {
body = authErrMsg(body, agentcfg.ErrMsgKibanaVersionNotCompatible, c.AuthResult.Authorized)
body = authErrMsg(c, body, agentcfg.ErrMsgKibanaVersionNotCompatible)
}
c.Result.Set(
request.IDResponseErrorsServiceUnavailable,
Expand All @@ -116,7 +139,7 @@ func (h *handler) Handle(c *request.Context) {
)
} else {
apm.CaptureError(c.Request.Context(), err).Send()
extractInternalError(c, err, c.AuthResult.Authorized)
extractInternalError(c, err)
}
c.Write()
return
Expand All @@ -135,12 +158,15 @@ func (h *handler) Handle(c *request.Context) {
c.Write()
}

func buildQuery(c *request.Context) (query agentcfg.Query, err error) {
func buildQuery(c *request.Context) (agentcfg.Query, error) {
r := c.Request

var query agentcfg.Query
switch r.Method {
case http.MethodPost:
err = convert.FromReader(r.Body, &query)
if err := convert.FromReader(r.Body, &query); err != nil {
return query, err
}
case http.MethodGet:
params := r.URL.Query()
query = agentcfg.Query{
Expand All @@ -150,41 +176,43 @@ func buildQuery(c *request.Context) (query agentcfg.Query, err error) {
},
}
default:
err = errors.Errorf("%s: %s", msgMethodUnsupported, r.Method)
if err := errors.Errorf("%s: %s", msgMethodUnsupported, r.Method); err != nil {
return query, err
}
}

if err == nil && query.Service.Name == "" {
err = errors.New(agentcfg.ServiceName + " is required")
if query.Service.Name == "" {
return query, errors.New(agentcfg.ServiceName + " is required")
}

if c.IsRum {
query.InsecureAgents = rumAgents
}
query.Etag = ifNoneMatch(c)
return
return query, nil
}

func extractInternalError(c *request.Context, err error, withAuth bool) {
func extractInternalError(c *request.Context, err error) {
msg := err.Error()
var body interface{}
var keyword string
switch {
case strings.Contains(msg, agentcfg.ErrMsgSendToKibanaFailed):
body = authErrMsg(msg, agentcfg.ErrMsgSendToKibanaFailed, withAuth)
body = authErrMsg(c, msg, agentcfg.ErrMsgSendToKibanaFailed)
keyword = agentcfg.ErrMsgSendToKibanaFailed

case strings.Contains(msg, agentcfg.ErrMsgReadKibanaResponse):
body = authErrMsg(msg, agentcfg.ErrMsgReadKibanaResponse, withAuth)
body = authErrMsg(c, msg, agentcfg.ErrMsgReadKibanaResponse)
keyword = agentcfg.ErrMsgReadKibanaResponse

case strings.Contains(msg, agentcfg.ErrUnauthorized):
fullMsg := "APM Server is not authorized to query Kibana. " +
"Please configure apm-server.kibana.username and apm-server.kibana.password, " +
"and ensure the user has the necessary privileges."
body = authErrMsg(fullMsg, agentcfg.ErrUnauthorized, withAuth)
body = authErrMsg(c, fullMsg, agentcfg.ErrUnauthorized)
keyword = agentcfg.ErrUnauthorized

default:
body = authErrMsg(msg, msgServiceUnavailable, withAuth)
body = authErrMsg(c, msg, msgServiceUnavailable)
keyword = msgServiceUnavailable
}

Expand All @@ -195,25 +223,25 @@ func extractInternalError(c *request.Context, err error, withAuth bool) {
err)
}

func extractQueryError(c *request.Context, err error, withAuth bool) {
func extractQueryError(c *request.Context, err error) {
msg := err.Error()
if strings.Contains(msg, msgMethodUnsupported) {
c.Result.Set(request.IDResponseErrorsMethodNotAllowed,
http.StatusMethodNotAllowed,
msgMethodUnsupported,
authErrMsg(msg, msgMethodUnsupported, withAuth),
authErrMsg(c, msg, msgMethodUnsupported),
err)
return
}
c.Result.Set(request.IDResponseErrorsInvalidQuery,
http.StatusBadRequest,
msgInvalidQuery,
authErrMsg(msg, msgInvalidQuery, withAuth),
authErrMsg(c, msg, msgInvalidQuery),
err)
}

func authErrMsg(fullMsg, shortMsg string, withAuth bool) string {
if withAuth {
func authErrMsg(c *request.Context, fullMsg, shortMsg string) string {
if !c.AuthResult.Anonymous {
return fullMsg
}
return shortMsg
Expand Down
Loading

0 comments on commit 57732ac

Please sign in to comment.