You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
consul/agent/ui_endpoint_oss_test.go

171 lines
4.8 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
//go:build !consulent
// +build !consulent
package agent
import (
"context"
"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) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
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()
a.enableDebug.Store(true)
h := a.srv.handler()
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(context.Background(), "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(context.Background(), "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))
}
})
}
})
}
}