Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unconditionally add Access-Control-Expose-Headers HTTP header #20220

Merged
merged 2 commits into from
Jan 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/20220.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
cloud: unconditionally add Access-Control-Expose-Headers HTTP header
```
35 changes: 24 additions & 11 deletions agent/hcp/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
loshz marked this conversation as resolved.
Show resolved Hide resolved
}
}

// 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.
Expand Down Expand Up @@ -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.
Expand Down
63 changes: 50 additions & 13 deletions agent/hcp/bootstrap/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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])
},
},
}
Expand Down
5 changes: 5 additions & 0 deletions command/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down