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..cc2cadfdfa93 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, nil + } +} + // 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())