From 5dda2790dc180ca865327563a4bb4d7b628b65c1 Mon Sep 17 00:00:00 2001 From: Nick Cellino Date: Tue, 16 Jan 2024 14:13:45 -0500 Subject: [PATCH 1/2] Unconditionally add Access-Control-Expose-Headers HTTP header --- .changelog/20220.txt | 3 ++ agent/hcp/bootstrap/bootstrap.go | 35 ++++++++++----- agent/hcp/bootstrap/bootstrap_test.go | 63 +++++++++++++++++++++------ command/agent/agent.go | 5 +++ 4 files changed, 82 insertions(+), 24 deletions(-) create mode 100644 .changelog/20220.txt diff --git a/.changelog/20220.txt b/.changelog/20220.txt new file mode 100644 index 000000000000..aca1a8096195 --- /dev/null +++ b/.changelog/20220.txt @@ -0,0 +1,3 @@ +```release-note:improvement +cloud: unconditionally add Access-Control-Expose-Headers HTTP header +``` diff --git a/agent/hcp/bootstrap/bootstrap.go b/agent/hcp/bootstrap/bootstrap.go index 8e544bdec312..32e591dab313 100644 --- a/agent/hcp/bootstrap/bootstrap.go +++ b/agent/hcp/bootstrap/bootstrap.go @@ -106,6 +106,30 @@ func LoadConfig(ctx context.Context, client hcpclient.Client, dataDir string, lo return newLoader, nil } +func AddAclPolicyAccessControlHeader(baseLoader ConfigLoader) ConfigLoader { + return func(source config.Source) (config.LoadResult, error) { + res, err := baseLoader(source) + if err != nil { + return res, err + } + + rc := res.RuntimeConfig + + // HTTP response headers are modified for the HCP UI to work. + if rc.HTTPResponseHeaders == nil { + rc.HTTPResponseHeaders = make(map[string]string) + } + prevValue, ok := rc.HTTPResponseHeaders[accessControlHeaderName] + if !ok { + rc.HTTPResponseHeaders[accessControlHeaderName] = accessControlHeaderValue + } else { + rc.HTTPResponseHeaders[accessControlHeaderName] = prevValue + "," + accessControlHeaderValue + } + + return res, err + } +} + // bootstrapConfigLoader is a ConfigLoader for passing bootstrap JSON config received from HCP // to the config.builder. ConfigLoaders are functions used to build an agent's RuntimeConfig // from various sources like files and flags. This config is contained in the config.LoadResult. @@ -166,17 +190,6 @@ const ( // handled by the config.builder. func finalizeRuntimeConfig(rc *config.RuntimeConfig, cfg *RawBootstrapConfig) { rc.Cloud.ManagementToken = cfg.ManagementToken - - // HTTP response headers are modified for the HCP UI to work. - if rc.HTTPResponseHeaders == nil { - rc.HTTPResponseHeaders = make(map[string]string) - } - prevValue, ok := rc.HTTPResponseHeaders[accessControlHeaderName] - if !ok { - rc.HTTPResponseHeaders[accessControlHeaderName] = accessControlHeaderValue - } else { - rc.HTTPResponseHeaders[accessControlHeaderName] = prevValue + "," + accessControlHeaderValue - } } // fetchBootstrapConfig will fetch boostrap configuration from remote servers and persist it to disk. diff --git a/agent/hcp/bootstrap/bootstrap_test.go b/agent/hcp/bootstrap/bootstrap_test.go index b475223ff8cf..da446cecf751 100644 --- a/agent/hcp/bootstrap/bootstrap_test.go +++ b/agent/hcp/bootstrap/bootstrap_test.go @@ -48,9 +48,6 @@ func TestBootstrapConfigLoader(t *testing.T) { // bootstrap_expect and management token are injected from bootstrap config received from HCP. require.Equal(t, 8, result.RuntimeConfig.BootstrapExpect) require.Equal(t, "test-token", result.RuntimeConfig.Cloud.ManagementToken) - - // Response header is always injected from a constant. - require.Equal(t, "x-consul-default-acl-policy", result.RuntimeConfig.HTTPResponseHeaders[accessControlHeaderName]) } func Test_finalizeRuntimeConfig(t *testing.T) { @@ -65,28 +62,68 @@ func Test_finalizeRuntimeConfig(t *testing.T) { } tt := map[string]testCase{ - "set header if not present": { + "set management token": { rc: &config.RuntimeConfig{}, cfg: &RawBootstrapConfig{ ManagementToken: "test-token", }, verifyFn: func(t *testing.T, rc *config.RuntimeConfig) { require.Equal(t, "test-token", rc.Cloud.ManagementToken) - require.Equal(t, "x-consul-default-acl-policy", rc.HTTPResponseHeaders[accessControlHeaderName]) }, }, + } + + for name, tc := range tt { + t.Run(name, func(t *testing.T) { + run(t, tc) + }) + } +} + +func Test_AddAclPolicyAccessControlHeader(t *testing.T) { + type testCase struct { + rc *config.RuntimeConfig + cfg *RawBootstrapConfig + baseLoader ConfigLoader + verifyFn func(t *testing.T, rc *config.RuntimeConfig) + } + run := func(t *testing.T, tc testCase) { + loader := AddAclPolicyAccessControlHeader(tc.baseLoader) + result, err := loader(nil) + require.NoError(t, err) + tc.verifyFn(t, result.RuntimeConfig) + } + + tt := map[string]testCase{ "append to header if present": { - rc: &config.RuntimeConfig{ - HTTPResponseHeaders: map[string]string{ - accessControlHeaderName: "Content-Encoding", - }, + baseLoader: func(source config.Source) (config.LoadResult, error) { + return config.Load(config.LoadOpts{ + DefaultConfig: config.DefaultSource(), + HCL: []string{ + `server = true`, + `bind_addr = "127.0.0.1"`, + `data_dir = "/tmp/consul-data"`, + fmt.Sprintf(`http_config = { response_headers = { %s = "test" } }`, accessControlHeaderName), + }, + }) }, - cfg: &RawBootstrapConfig{ - ManagementToken: "test-token", + verifyFn: func(t *testing.T, rc *config.RuntimeConfig) { + require.Equal(t, "test,x-consul-default-acl-policy", rc.HTTPResponseHeaders[accessControlHeaderName]) + }, + }, + "set header if not present": { + baseLoader: func(source config.Source) (config.LoadResult, error) { + return config.Load(config.LoadOpts{ + DefaultConfig: config.DefaultSource(), + HCL: []string{ + `server = true`, + `bind_addr = "127.0.0.1"`, + `data_dir = "/tmp/consul-data"`, + }, + }) }, verifyFn: func(t *testing.T, rc *config.RuntimeConfig) { - require.Equal(t, "test-token", rc.Cloud.ManagementToken) - require.Equal(t, "Content-Encoding,x-consul-default-acl-policy", rc.HTTPResponseHeaders[accessControlHeaderName]) + require.Equal(t, "x-consul-default-acl-policy", rc.HTTPResponseHeaders[accessControlHeaderName]) }, }, } diff --git a/command/agent/agent.go b/command/agent/agent.go index 84515f2c94bd..4b06e98edd7f 100644 --- a/command/agent/agent.go +++ b/command/agent/agent.go @@ -183,6 +183,11 @@ func (c *cmd) run(args []string) int { } } + // We unconditionally add an Access Control header to our config in order to allow the HCP UI to work. + // We do this unconditionally because the cluster can be linked to HCP at any time (not just at startup) and this + // is simpler than selectively reloading parts of config at runtime. + loader = hcpbootstrap.AddAclPolicyAccessControlHeader(loader) + bd, err := agent.NewBaseDeps(loader, logGate, nil) if err != nil { ui.Error(err.Error()) From 85c7edda8885af4afde2ca03e437040f296091f5 Mon Sep 17 00:00:00 2001 From: Nick Cellino Date: Thu, 18 Jan 2024 09:36:14 -0500 Subject: [PATCH 2/2] Return nil instead of err --- agent/hcp/bootstrap/bootstrap.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agent/hcp/bootstrap/bootstrap.go b/agent/hcp/bootstrap/bootstrap.go index 32e591dab313..cc2cadfdfa93 100644 --- a/agent/hcp/bootstrap/bootstrap.go +++ b/agent/hcp/bootstrap/bootstrap.go @@ -126,7 +126,7 @@ func AddAclPolicyAccessControlHeader(baseLoader ConfigLoader) ConfigLoader { rc.HTTPResponseHeaders[accessControlHeaderName] = prevValue + "," + accessControlHeaderValue } - return res, err + return res, nil } }