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.
160 lines
4.6 KiB
Go
160 lines
4.6 KiB
Go
// +build !consulent
|
|
|
|
package agent
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"sync/atomic"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
"github.com/hashicorp/consul/testrpc"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestUIEndpoint_MetricsProxy_ACLDeny(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
var (
|
|
lastHeadersSent atomic.Value
|
|
backendCalled atomic.Value
|
|
)
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
backendCalled.Store(true)
|
|
lastHeadersSent.Store(r.Header)
|
|
if r.URL.Path == "/some/prefix/ok" {
|
|
w.Write([]byte("OK"))
|
|
return
|
|
}
|
|
http.Error(w, "not found on backend", http.StatusNotFound)
|
|
}))
|
|
defer backend.Close()
|
|
|
|
backendURL := backend.URL + "/some/prefix"
|
|
|
|
a := NewTestAgent(t, TestACLConfig()+fmt.Sprintf(`
|
|
ui_config {
|
|
enabled = true
|
|
metrics_proxy {
|
|
base_url = %q
|
|
}
|
|
}
|
|
http_config {
|
|
response_headers {
|
|
"Access-Control-Allow-Origin" = "*"
|
|
}
|
|
}
|
|
`, backendURL))
|
|
defer a.Shutdown()
|
|
|
|
h := a.srv.handler(true)
|
|
|
|
testrpc.WaitForLeader(t, a.RPC, "dc1")
|
|
|
|
const endpointPath = "/v1/internal/ui/metrics-proxy"
|
|
|
|
// create some ACL things
|
|
for name, rules := range map[string]string{
|
|
"one-service": `service "foo" { policy = "read" }`,
|
|
"all-services": `service_prefix "" { policy = "read" }`,
|
|
"one-node": `node "bar" { policy = "read" }`,
|
|
"all-nodes": `node_prefix "" { policy = "read" }`,
|
|
} {
|
|
req := structs.ACLPolicySetRequest{
|
|
Policy: structs.ACLPolicy{
|
|
Name: name,
|
|
Rules: rules,
|
|
},
|
|
Datacenter: "dc1",
|
|
WriteRequest: structs.WriteRequest{Token: "root"},
|
|
}
|
|
var policy structs.ACLPolicy
|
|
require.NoError(t, a.RPC("ACL.PolicySet", &req, &policy))
|
|
}
|
|
|
|
makeToken := func(t *testing.T, policyNames []string) string {
|
|
req := structs.ACLTokenSetRequest{
|
|
ACLToken: structs.ACLToken{},
|
|
Datacenter: "dc1",
|
|
WriteRequest: structs.WriteRequest{Token: "root"},
|
|
}
|
|
for _, name := range policyNames {
|
|
req.ACLToken.Policies = append(req.ACLToken.Policies, structs.ACLTokenPolicyLink{Name: name})
|
|
}
|
|
require.Len(t, req.ACLToken.Policies, len(policyNames))
|
|
|
|
var token structs.ACLToken
|
|
require.NoError(t, a.RPC("ACL.TokenSet", &req, &token))
|
|
return token.SecretID
|
|
}
|
|
|
|
type testcase struct {
|
|
name string
|
|
token string
|
|
policies []string
|
|
expect int
|
|
}
|
|
|
|
for _, tc := range []testcase{
|
|
{name: "no token", token: "", expect: http.StatusForbidden},
|
|
{name: "root token", token: "root", expect: http.StatusOK},
|
|
//
|
|
{name: "one node", policies: []string{"one-node"}, expect: http.StatusForbidden},
|
|
{name: "all nodes", policies: []string{"all-nodes"}, expect: http.StatusForbidden},
|
|
//
|
|
{name: "one service", policies: []string{"one-service"}, expect: http.StatusForbidden},
|
|
{name: "all services", policies: []string{"all-services"}, expect: http.StatusForbidden},
|
|
//
|
|
{name: "one service one node", policies: []string{"one-service", "one-node"}, expect: http.StatusForbidden},
|
|
{name: "all services one node", policies: []string{"all-services", "one-node"}, expect: http.StatusForbidden},
|
|
//
|
|
{name: "one service all nodes", policies: []string{"one-service", "one-node"}, expect: http.StatusForbidden},
|
|
{name: "all services all nodes", policies: []string{"all-services", "all-nodes"}, expect: http.StatusOK},
|
|
} {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if tc.token == "" {
|
|
tc.token = makeToken(t, tc.policies)
|
|
}
|
|
|
|
t.Run("via query param should not work", func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", endpointPath+"/ok?token="+tc.token, nil)
|
|
rec := httptest.NewRecorder()
|
|
backendCalled.Store(false)
|
|
h.ServeHTTP(rec, req)
|
|
require.Equal(t, http.StatusForbidden, rec.Code)
|
|
|
|
require.False(t, backendCalled.Load().(bool))
|
|
})
|
|
|
|
for _, headerName := range []string{"x-consul-token", "authorization"} {
|
|
headerVal := tc.token
|
|
if headerName == "authorization" {
|
|
headerVal = "bearer " + tc.token
|
|
}
|
|
|
|
t.Run("via header "+headerName, func(t *testing.T) {
|
|
req := httptest.NewRequest("GET", endpointPath+"/ok", nil)
|
|
req.Header.Set(headerName, headerVal)
|
|
rec := httptest.NewRecorder()
|
|
backendCalled.Store(false)
|
|
h.ServeHTTP(rec, req)
|
|
require.Equal(t, tc.expect, rec.Code)
|
|
|
|
headersSent, _ := lastHeadersSent.Load().(http.Header)
|
|
if tc.expect == http.StatusOK {
|
|
require.True(t, backendCalled.Load().(bool))
|
|
// Ensure we didn't accidentally ship our consul token to the proxy.
|
|
require.Empty(t, headersSent.Get("X-Consul-Token"))
|
|
require.Empty(t, headersSent.Get("Authorization"))
|
|
} else {
|
|
require.False(t, backendCalled.Load().(bool))
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|