c2d167d06e
This ensures the metrics proxy endpoint is ACL protected behind a wildcard `service:read` and `node:read` set of rules. For Consul Enterprise these will need to span all namespaces: ``` service_prefix "" { policy = "read" } node_prefix "" { policy = "read" } namespace_prefix "" { service_prefix "" { policy = "read" } node_prefix "" { policy = "read" } } ``` This PR contains just the backend changes. The frontend changes to actually pass the consul token header to the proxy through the JS plugin will come in another PR.
684 lines
20 KiB
Go
684 lines
20 KiB
Go
package agent
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httputil"
|
|
"net/url"
|
|
"path"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/consul/acl"
|
|
"github.com/hashicorp/consul/agent/config"
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
"github.com/hashicorp/consul/api"
|
|
"github.com/hashicorp/consul/logging"
|
|
"github.com/hashicorp/go-hclog"
|
|
)
|
|
|
|
// ServiceSummary is used to summarize a service
|
|
type ServiceSummary struct {
|
|
Kind structs.ServiceKind `json:",omitempty"`
|
|
Name string
|
|
Datacenter string
|
|
Tags []string
|
|
Nodes []string
|
|
ExternalSources []string
|
|
externalSourceSet map[string]struct{} // internal to track uniqueness
|
|
checks map[string]*structs.HealthCheck
|
|
InstanceCount int
|
|
ChecksPassing int
|
|
ChecksWarning int
|
|
ChecksCritical int
|
|
GatewayConfig GatewayConfig
|
|
|
|
structs.EnterpriseMeta
|
|
}
|
|
|
|
func (s *ServiceSummary) LessThan(other *ServiceSummary) bool {
|
|
if s.EnterpriseMeta.LessThan(&other.EnterpriseMeta) {
|
|
return true
|
|
}
|
|
return s.Name < other.Name
|
|
}
|
|
|
|
type GatewayConfig struct {
|
|
AssociatedServiceCount int `json:",omitempty"`
|
|
Addresses []string `json:",omitempty"`
|
|
|
|
// internal to track uniqueness
|
|
addressesSet map[string]struct{}
|
|
}
|
|
|
|
type ServiceListingSummary struct {
|
|
ServiceSummary
|
|
|
|
ConnectedWithProxy bool
|
|
ConnectedWithGateway bool
|
|
}
|
|
|
|
type ServiceTopologySummary struct {
|
|
ServiceSummary
|
|
|
|
Intention structs.IntentionDecisionSummary
|
|
}
|
|
|
|
type ServiceTopology struct {
|
|
Protocol string
|
|
Upstreams []*ServiceTopologySummary
|
|
Downstreams []*ServiceTopologySummary
|
|
FilteredByACLs bool
|
|
}
|
|
|
|
// UINodes is used to list the nodes in a given datacenter. We return a
|
|
// NodeDump which provides overview information for all the nodes
|
|
func (s *HTTPHandlers) UINodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
// Parse arguments
|
|
args := structs.DCSpecificRequest{}
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
return nil, nil
|
|
}
|
|
|
|
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s.parseFilter(req, &args.Filter)
|
|
|
|
// Make the RPC request
|
|
var out structs.IndexedNodeDump
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
RPC:
|
|
if err := s.agent.RPC("Internal.NodeDump", &args, &out); err != nil {
|
|
// Retry the request allowing stale data if no leader
|
|
if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale {
|
|
args.AllowStale = true
|
|
goto RPC
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// Use empty list instead of nil
|
|
for _, info := range out.Dump {
|
|
if info.Services == nil {
|
|
info.Services = make([]*structs.NodeService, 0)
|
|
}
|
|
if info.Checks == nil {
|
|
info.Checks = make([]*structs.HealthCheck, 0)
|
|
}
|
|
}
|
|
if out.Dump == nil {
|
|
out.Dump = make(structs.NodeDump, 0)
|
|
}
|
|
return out.Dump, nil
|
|
}
|
|
|
|
// UINodeInfo is used to get info on a single node in a given datacenter. We return a
|
|
// NodeInfo which provides overview information for the node
|
|
func (s *HTTPHandlers) UINodeInfo(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
// Parse arguments
|
|
args := structs.NodeSpecificRequest{}
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
return nil, nil
|
|
}
|
|
|
|
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Verify we have some DC, or use the default
|
|
args.Node = strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/node/")
|
|
if args.Node == "" {
|
|
resp.WriteHeader(http.StatusBadRequest)
|
|
fmt.Fprint(resp, "Missing node name")
|
|
return nil, nil
|
|
}
|
|
|
|
// Make the RPC request
|
|
var out structs.IndexedNodeDump
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
RPC:
|
|
if err := s.agent.RPC("Internal.NodeInfo", &args, &out); err != nil {
|
|
// Retry the request allowing stale data if no leader
|
|
if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale {
|
|
args.AllowStale = true
|
|
goto RPC
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// Return only the first entry
|
|
if len(out.Dump) > 0 {
|
|
info := out.Dump[0]
|
|
if info.Services == nil {
|
|
info.Services = make([]*structs.NodeService, 0)
|
|
}
|
|
if info.Checks == nil {
|
|
info.Checks = make([]*structs.HealthCheck, 0)
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
resp.WriteHeader(http.StatusNotFound)
|
|
return nil, nil
|
|
}
|
|
|
|
// UIServices is used to list the services in a given datacenter. We return a
|
|
// ServiceSummary which provides overview information for the service
|
|
func (s *HTTPHandlers) UIServices(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
// Parse arguments
|
|
args := structs.ServiceDumpRequest{}
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
return nil, nil
|
|
}
|
|
|
|
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s.parseFilter(req, &args.Filter)
|
|
|
|
// Make the RPC request
|
|
var out structs.IndexedNodesWithGateways
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
RPC:
|
|
if err := s.agent.RPC("Internal.ServiceDump", &args, &out); err != nil {
|
|
// Retry the request allowing stale data if no leader
|
|
if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale {
|
|
args.AllowStale = true
|
|
goto RPC
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
// Store the names of the gateways associated with each service
|
|
var (
|
|
serviceGateways = make(map[structs.ServiceName][]structs.ServiceName)
|
|
numLinkedServices = make(map[structs.ServiceName]int)
|
|
)
|
|
for _, gs := range out.Gateways {
|
|
serviceGateways[gs.Service] = append(serviceGateways[gs.Service], gs.Gateway)
|
|
numLinkedServices[gs.Gateway] += 1
|
|
}
|
|
|
|
summaries, hasProxy := summarizeServices(out.Nodes.ToServiceDump(), nil, "")
|
|
sorted := prepSummaryOutput(summaries, false)
|
|
|
|
var result []*ServiceListingSummary
|
|
for _, svc := range sorted {
|
|
sum := ServiceListingSummary{ServiceSummary: *svc}
|
|
|
|
sn := structs.NewServiceName(svc.Name, &svc.EnterpriseMeta)
|
|
if hasProxy[sn] {
|
|
sum.ConnectedWithProxy = true
|
|
}
|
|
|
|
// Verify that at least one of the gateways linked by config entry has an instance registered in the catalog
|
|
for _, gw := range serviceGateways[sn] {
|
|
if s := summaries[gw]; s != nil && sum.InstanceCount > 0 {
|
|
sum.ConnectedWithGateway = true
|
|
}
|
|
}
|
|
sum.GatewayConfig.AssociatedServiceCount = numLinkedServices[sn]
|
|
|
|
result = append(result, &sum)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// UIGatewayServices is used to query all the nodes for services associated with a gateway along with their gateway config
|
|
func (s *HTTPHandlers) UIGatewayServicesNodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
// Parse arguments
|
|
args := structs.ServiceSpecificRequest{}
|
|
if err := s.parseEntMetaNoWildcard(req, &args.EnterpriseMeta); err != nil {
|
|
return nil, err
|
|
}
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
return nil, nil
|
|
}
|
|
|
|
// Pull out the service name
|
|
args.ServiceName = strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/gateway-services-nodes/")
|
|
if args.ServiceName == "" {
|
|
resp.WriteHeader(http.StatusBadRequest)
|
|
fmt.Fprint(resp, "Missing gateway name")
|
|
return nil, nil
|
|
}
|
|
|
|
// Make the RPC request
|
|
var out structs.IndexedServiceDump
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
RPC:
|
|
if err := s.agent.RPC("Internal.GatewayServiceDump", &args, &out); err != nil {
|
|
// Retry the request allowing stale data if no leader
|
|
if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale {
|
|
args.AllowStale = true
|
|
goto RPC
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
summaries, _ := summarizeServices(out.Dump, s.agent.config, args.Datacenter)
|
|
return prepSummaryOutput(summaries, false), nil
|
|
}
|
|
|
|
// UIServiceTopology returns the list of upstreams and downstreams for a Connect enabled service.
|
|
// - Downstreams are services that list the given service as an upstream
|
|
// - Upstreams are the upstreams defined in the given service's proxy registrations
|
|
func (s *HTTPHandlers) UIServiceTopology(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
// Parse arguments
|
|
args := structs.ServiceSpecificRequest{}
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
return nil, nil
|
|
}
|
|
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
args.ServiceName = strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/service-topology/")
|
|
if args.ServiceName == "" {
|
|
resp.WriteHeader(http.StatusBadRequest)
|
|
fmt.Fprint(resp, "Missing service name")
|
|
return nil, nil
|
|
}
|
|
|
|
kind, ok := req.URL.Query()["kind"]
|
|
if !ok {
|
|
resp.WriteHeader(http.StatusBadRequest)
|
|
fmt.Fprint(resp, "Missing service kind")
|
|
return nil, nil
|
|
}
|
|
args.ServiceKind = structs.ServiceKind(kind[0])
|
|
|
|
switch args.ServiceKind {
|
|
case structs.ServiceKindTypical, structs.ServiceKindIngressGateway:
|
|
// allowed
|
|
default:
|
|
resp.WriteHeader(http.StatusBadRequest)
|
|
fmt.Fprintf(resp, "Unsupported service kind %q", args.ServiceKind)
|
|
return nil, nil
|
|
}
|
|
|
|
// Make the RPC request
|
|
var out structs.IndexedServiceTopology
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
RPC:
|
|
if err := s.agent.RPC("Internal.ServiceTopology", &args, &out); err != nil {
|
|
// Retry the request allowing stale data if no leader
|
|
if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale {
|
|
args.AllowStale = true
|
|
goto RPC
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
upstreams, _ := summarizeServices(out.ServiceTopology.Upstreams.ToServiceDump(), nil, "")
|
|
downstreams, _ := summarizeServices(out.ServiceTopology.Downstreams.ToServiceDump(), nil, "")
|
|
|
|
var (
|
|
upstreamResp = make([]*ServiceTopologySummary, 0)
|
|
downstreamResp = make([]*ServiceTopologySummary, 0)
|
|
)
|
|
|
|
// Sort and attach intention data for upstreams and downstreams
|
|
sortedUpstreams := prepSummaryOutput(upstreams, true)
|
|
for _, svc := range sortedUpstreams {
|
|
sn := structs.NewServiceName(svc.Name, &svc.EnterpriseMeta)
|
|
sum := ServiceTopologySummary{
|
|
ServiceSummary: *svc,
|
|
Intention: out.ServiceTopology.UpstreamDecisions[sn.String()],
|
|
}
|
|
upstreamResp = append(upstreamResp, &sum)
|
|
}
|
|
|
|
sortedDownstreams := prepSummaryOutput(downstreams, true)
|
|
for _, svc := range sortedDownstreams {
|
|
sn := structs.NewServiceName(svc.Name, &svc.EnterpriseMeta)
|
|
sum := ServiceTopologySummary{
|
|
ServiceSummary: *svc,
|
|
Intention: out.ServiceTopology.DownstreamDecisions[sn.String()],
|
|
}
|
|
downstreamResp = append(downstreamResp, &sum)
|
|
}
|
|
|
|
topo := ServiceTopology{
|
|
Protocol: out.ServiceTopology.MetricsProtocol,
|
|
Upstreams: upstreamResp,
|
|
Downstreams: downstreamResp,
|
|
FilteredByACLs: out.FilteredByACLs,
|
|
}
|
|
return topo, nil
|
|
}
|
|
|
|
func summarizeServices(dump structs.ServiceDump, cfg *config.RuntimeConfig, dc string) (map[structs.ServiceName]*ServiceSummary, map[structs.ServiceName]bool) {
|
|
var (
|
|
summary = make(map[structs.ServiceName]*ServiceSummary)
|
|
hasProxy = make(map[structs.ServiceName]bool)
|
|
)
|
|
|
|
getService := func(service structs.ServiceName) *ServiceSummary {
|
|
serv, ok := summary[service]
|
|
if !ok {
|
|
serv = &ServiceSummary{
|
|
Name: service.Name,
|
|
EnterpriseMeta: service.EnterpriseMeta,
|
|
// the other code will increment this unconditionally so we
|
|
// shouldn't initialize it to 1
|
|
InstanceCount: 0,
|
|
}
|
|
summary[service] = serv
|
|
}
|
|
return serv
|
|
}
|
|
|
|
for _, csn := range dump {
|
|
if cfg != nil && csn.GatewayService != nil {
|
|
gwsvc := csn.GatewayService
|
|
sum := getService(gwsvc.Service)
|
|
modifySummaryForGatewayService(cfg, dc, sum, gwsvc)
|
|
}
|
|
|
|
// Will happen in cases where we only have the GatewayServices mapping
|
|
if csn.Service == nil {
|
|
continue
|
|
}
|
|
sn := structs.NewServiceName(csn.Service.Service, &csn.Service.EnterpriseMeta)
|
|
sum := getService(sn)
|
|
|
|
svc := csn.Service
|
|
sum.Nodes = append(sum.Nodes, csn.Node.Node)
|
|
sum.Kind = svc.Kind
|
|
sum.Datacenter = csn.Node.Datacenter
|
|
sum.InstanceCount += 1
|
|
if svc.Kind == structs.ServiceKindConnectProxy {
|
|
sn := structs.NewServiceName(svc.Proxy.DestinationServiceName, &svc.EnterpriseMeta)
|
|
hasProxy[sn] = true
|
|
|
|
destination := getService(sn)
|
|
for _, check := range csn.Checks {
|
|
cid := structs.NewCheckID(check.CheckID, &check.EnterpriseMeta)
|
|
uid := structs.UniqueID(csn.Node.Node, cid.String())
|
|
if destination.checks == nil {
|
|
destination.checks = make(map[string]*structs.HealthCheck)
|
|
}
|
|
destination.checks[uid] = check
|
|
}
|
|
}
|
|
for _, tag := range svc.Tags {
|
|
found := false
|
|
for _, existing := range sum.Tags {
|
|
if existing == tag {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
sum.Tags = append(sum.Tags, tag)
|
|
}
|
|
}
|
|
|
|
// If there is an external source, add it to the list of external
|
|
// sources. We only want to add unique sources so there is extra
|
|
// accounting here with an unexported field to maintain the set
|
|
// of sources.
|
|
if len(svc.Meta) > 0 && svc.Meta[structs.MetaExternalSource] != "" {
|
|
source := svc.Meta[structs.MetaExternalSource]
|
|
if sum.externalSourceSet == nil {
|
|
sum.externalSourceSet = make(map[string]struct{})
|
|
}
|
|
if _, ok := sum.externalSourceSet[source]; !ok {
|
|
sum.externalSourceSet[source] = struct{}{}
|
|
sum.ExternalSources = append(sum.ExternalSources, source)
|
|
}
|
|
}
|
|
|
|
for _, check := range csn.Checks {
|
|
cid := structs.NewCheckID(check.CheckID, &check.EnterpriseMeta)
|
|
uid := structs.UniqueID(csn.Node.Node, cid.String())
|
|
if sum.checks == nil {
|
|
sum.checks = make(map[string]*structs.HealthCheck)
|
|
}
|
|
sum.checks[uid] = check
|
|
}
|
|
}
|
|
|
|
return summary, hasProxy
|
|
}
|
|
|
|
func prepSummaryOutput(summaries map[structs.ServiceName]*ServiceSummary, excludeSidecars bool) []*ServiceSummary {
|
|
var resp []*ServiceSummary
|
|
|
|
// Collect and sort resp for display
|
|
for _, sum := range summaries {
|
|
sort.Strings(sum.Nodes)
|
|
sort.Strings(sum.Tags)
|
|
|
|
for _, chk := range sum.checks {
|
|
switch chk.Status {
|
|
case api.HealthPassing:
|
|
sum.ChecksPassing++
|
|
case api.HealthWarning:
|
|
sum.ChecksWarning++
|
|
case api.HealthCritical:
|
|
sum.ChecksCritical++
|
|
}
|
|
}
|
|
if excludeSidecars && sum.Kind != structs.ServiceKindTypical && sum.Kind != structs.ServiceKindIngressGateway {
|
|
continue
|
|
}
|
|
resp = append(resp, sum)
|
|
}
|
|
sort.Slice(resp, func(i, j int) bool {
|
|
return resp[i].LessThan(resp[j])
|
|
})
|
|
return resp
|
|
}
|
|
|
|
func modifySummaryForGatewayService(
|
|
cfg *config.RuntimeConfig,
|
|
datacenter string,
|
|
sum *ServiceSummary,
|
|
gwsvc *structs.GatewayService,
|
|
) {
|
|
var dnsAddresses []string
|
|
for _, domain := range []string{cfg.DNSDomain, cfg.DNSAltDomain} {
|
|
// If the domain is empty, do not use it to construct a valid DNS
|
|
// address
|
|
if domain == "" {
|
|
continue
|
|
}
|
|
dnsAddresses = append(dnsAddresses, serviceIngressDNSName(
|
|
gwsvc.Service.Name,
|
|
datacenter,
|
|
domain,
|
|
&gwsvc.Service.EnterpriseMeta,
|
|
))
|
|
}
|
|
|
|
for _, addr := range gwsvc.Addresses(dnsAddresses) {
|
|
// check for duplicates, a service will have a ServiceInfo struct for
|
|
// every instance that is registered.
|
|
if _, ok := sum.GatewayConfig.addressesSet[addr]; !ok {
|
|
if sum.GatewayConfig.addressesSet == nil {
|
|
sum.GatewayConfig.addressesSet = make(map[string]struct{})
|
|
}
|
|
sum.GatewayConfig.addressesSet[addr] = struct{}{}
|
|
sum.GatewayConfig.Addresses = append(
|
|
sum.GatewayConfig.Addresses, addr,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// GET /v1/internal/ui/gateway-intentions/:gateway
|
|
func (s *HTTPHandlers) UIGatewayIntentions(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
var args structs.IntentionQueryRequest
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
return nil, nil
|
|
}
|
|
|
|
var entMeta structs.EnterpriseMeta
|
|
if err := s.parseEntMetaNoWildcard(req, &entMeta); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Pull out the service name
|
|
name := strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/gateway-intentions/")
|
|
if name == "" {
|
|
resp.WriteHeader(http.StatusBadRequest)
|
|
fmt.Fprint(resp, "Missing gateway name")
|
|
return nil, nil
|
|
}
|
|
args.Match = &structs.IntentionQueryMatch{
|
|
Type: structs.IntentionMatchDestination,
|
|
Entries: []structs.IntentionMatchEntry{
|
|
{
|
|
Namespace: entMeta.NamespaceOrEmpty(),
|
|
Name: name,
|
|
},
|
|
},
|
|
}
|
|
|
|
var reply structs.IndexedIntentions
|
|
|
|
defer setMeta(resp, &reply.QueryMeta)
|
|
if err := s.agent.RPC("Internal.GatewayIntentions", args, &reply); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return reply.Intentions, nil
|
|
}
|
|
|
|
// UIMetricsProxy handles the /v1/internal/ui/metrics-proxy/ endpoint which, if
|
|
// configured, provides a simple read-only HTTP proxy to a single metrics
|
|
// backend to expose it to the UI.
|
|
func (s *HTTPHandlers) UIMetricsProxy(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
// Check the UI was enabled at agent startup (note this is not reloadable
|
|
// currently).
|
|
if !s.IsUIEnabled() {
|
|
return nil, NotFoundError{Reason: "UI is not enabled"}
|
|
}
|
|
|
|
// Load reloadable proxy config
|
|
cfg, ok := s.metricsProxyCfg.Load().(config.UIMetricsProxy)
|
|
if !ok || cfg.BaseURL == "" {
|
|
// Proxy not configured
|
|
return nil, NotFoundError{Reason: "Metrics proxy is not enabled"}
|
|
}
|
|
|
|
// Fetch the ACL token, if provided, but ONLY from headers since other
|
|
// metrics proxies might use a ?token query string parameter for something.
|
|
var token string
|
|
s.parseTokenFromHeaders(req, &token)
|
|
|
|
// Clear the token from the headers so we don't end up proxying it.
|
|
s.clearTokenFromHeaders(req)
|
|
|
|
var entMeta structs.EnterpriseMeta
|
|
authz, err := s.agent.resolveTokenAndDefaultMeta(token, &entMeta, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if authz != nil {
|
|
// This endpoint requires wildcard read on all services and all nodes.
|
|
//
|
|
// In enterprise it requires this _in all namespaces_ too.
|
|
wildMeta := structs.WildcardEnterpriseMeta()
|
|
var authzContext acl.AuthorizerContext
|
|
wildMeta.FillAuthzContext(&authzContext)
|
|
|
|
if authz.NodeReadAll(&authzContext) != acl.Allow || authz.ServiceReadAll(&authzContext) != acl.Allow {
|
|
return nil, acl.ErrPermissionDenied
|
|
}
|
|
}
|
|
|
|
log := s.agent.logger.Named(logging.UIMetricsProxy)
|
|
|
|
// Construct the new URL from the path and the base path. Note we do this here
|
|
// not in the Director function below because we can handle any errors cleanly
|
|
// here.
|
|
|
|
// Replace prefix in the path
|
|
subPath := strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/metrics-proxy")
|
|
|
|
// Append that to the BaseURL (which might contain a path prefix component)
|
|
newURL := cfg.BaseURL + subPath
|
|
|
|
// Parse it into a new URL
|
|
u, err := url.Parse(newURL)
|
|
if err != nil {
|
|
log.Error("couldn't parse target URL", "base_url", cfg.BaseURL, "path", subPath)
|
|
return nil, BadRequestError{Reason: "Invalid path."}
|
|
}
|
|
|
|
// Clean the new URL path to prevent path traversal attacks and remove any
|
|
// double slashes etc.
|
|
u.Path = path.Clean(u.Path)
|
|
|
|
if len(cfg.PathAllowlist) > 0 {
|
|
// This could be done better with a map, but for the prometheus default
|
|
// integration this list has two items in it, so the straight iteration
|
|
// isn't awful.
|
|
denied := true
|
|
for _, allowedPath := range cfg.PathAllowlist {
|
|
if u.Path == allowedPath {
|
|
denied = false
|
|
break
|
|
}
|
|
}
|
|
if denied {
|
|
log.Error("target URL path is not allowed",
|
|
"base_url", cfg.BaseURL,
|
|
"path", subPath,
|
|
"target_url", u.String(),
|
|
"path_allowlist", cfg.PathAllowlist,
|
|
)
|
|
resp.WriteHeader(http.StatusForbidden)
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
// Pass through query params
|
|
u.RawQuery = req.URL.RawQuery
|
|
|
|
// Validate that the full BaseURL is still a prefix - if there was a path
|
|
// prefix on the BaseURL but an attacker tried to circumvent it with path
|
|
// traversal then the Clean above would have resolve the /../ components back
|
|
// to the actual path which means part of the prefix will now be missing.
|
|
//
|
|
// Note that in practice this is not currently possible since any /../ in the
|
|
// path would have already been resolved by the API server mux and so not even
|
|
// hit this handler. Any /../ that are far enough into the path to hit this
|
|
// handler, can't backtrack far enough to eat into the BaseURL either. But we
|
|
// leave this in anyway in case something changes in the future.
|
|
if !strings.HasPrefix(u.String(), cfg.BaseURL) {
|
|
log.Error("target URL escaped from base path",
|
|
"base_url", cfg.BaseURL,
|
|
"path", subPath,
|
|
"target_url", u.String(),
|
|
)
|
|
return nil, BadRequestError{Reason: "Invalid path."}
|
|
}
|
|
|
|
// Add any configured headers
|
|
for _, h := range cfg.AddHeaders {
|
|
req.Header.Set(h.Name, h.Value)
|
|
}
|
|
|
|
log.Debug("proxying request", "to", u.String())
|
|
|
|
proxy := httputil.ReverseProxy{
|
|
Director: func(r *http.Request) {
|
|
r.URL = u
|
|
},
|
|
ErrorLog: log.StandardLogger(&hclog.StandardLoggerOptions{
|
|
InferLevels: true,
|
|
}),
|
|
}
|
|
|
|
proxy.ServeHTTP(resp, req)
|
|
return nil, nil
|
|
}
|