mirror of https://github.com/hashicorp/consul
171 lines
4.8 KiB
Go
171 lines
4.8 KiB
Go
|
// Copyright (c) HashiCorp, Inc.
|
||
|
// SPDX-License-Identifier: BUSL-1.1
|
||
|
|
||
|
//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))
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
}
|