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))
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|