diff --git a/changelog/10613.txt b/changelog/10613.txt new file mode 100644 index 000000000..276b32941 --- /dev/null +++ b/changelog/10613.txt @@ -0,0 +1,3 @@ +```release-node:improvement +core: Added an internal endpoint that lists feature flags. +``` diff --git a/http/handler.go b/http/handler.go index 5ab7d74e5..f6e9ee034 100644 --- a/http/handler.go +++ b/http/handler.go @@ -146,6 +146,7 @@ func Handler(props *vault.HandlerProperties) http.Handler { mux.Handle("/v1/sys/rekey-recovery-key/verify", handleRequestForwarding(core, handleSysRekeyVerify(core, true))) mux.Handle("/v1/sys/storage/raft/bootstrap", handleSysRaftBootstrap(core)) mux.Handle("/v1/sys/storage/raft/join", handleSysRaftJoin(core)) + mux.Handle("/v1/sys/internal/ui/feature-flags", handleSysInternalFeatureFlags(core)) for _, path := range injectDataIntoTopRoutes { mux.Handle(path, handleRequestForwarding(core, handleLogicalWithInjector(core))) } diff --git a/http/sys_feature_flags.go b/http/sys_feature_flags.go new file mode 100644 index 000000000..11ece3279 --- /dev/null +++ b/http/sys_feature_flags.go @@ -0,0 +1,52 @@ +package http + +import ( + "encoding/json" + "net/http" + "os" + + "github.com/hashicorp/vault/vault" +) + +type FeatureFlagsResponse struct { + FeatureFlags []string `json:"feature_flags"` +} + +var FeatureFlag_EnvVariables = [...]string{ + "VAULT_CLOUD_ADMIN_NAMESPACE", +} + +func featureFlagIsSet(name string) bool { + switch os.Getenv(name) { + case "", "0": + return false + default: + return true + } +} + +func handleSysInternalFeatureFlags(core *vault.Core) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case "GET": + break + default: + respondError(w, http.StatusMethodNotAllowed, nil) + } + + response := &FeatureFlagsResponse{} + + for _, f := range FeatureFlag_EnvVariables { + if featureFlagIsSet(f) { + response.FeatureFlags = append(response.FeatureFlags, f) + } + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + // Generate the response + enc := json.NewEncoder(w) + enc.Encode(response) + }) +} diff --git a/vault/external_tests/api/feature_flag_ext_test.go b/vault/external_tests/api/feature_flag_ext_test.go new file mode 100644 index 000000000..8b72b1e6e --- /dev/null +++ b/vault/external_tests/api/feature_flag_ext_test.go @@ -0,0 +1,93 @@ +package api + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "os" + "testing" + + "github.com/hashicorp/go-cleanhttp" + vaulthttp "github.com/hashicorp/vault/http" + "github.com/hashicorp/vault/vault" + "golang.org/x/net/http2" +) + +func TestFeatureFlags(t *testing.T) { + cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ + HandlerFunc: vaulthttp.Handler, + }) + cluster.Start() + defer cluster.Cleanup() + + // Wait for core to start + core := cluster.Cores[0].Core + vault.TestWaitActive(t, core) + client := cluster.Cores[0].Client + + // Create a raw http connection copying the configuration + // created by NewTestCluster + transport := cleanhttp.DefaultPooledTransport() + transport.TLSClientConfig = cluster.Cores[0].TLSConfig.Clone() + if err := http2.ConfigureTransport(transport); err != nil { + t.Fatal(err) + } + httpClient := &http.Client{ + Transport: transport, + } + + callApi := func() map[string]interface{} { + // Use the normal API client to construct the URL + req := client.NewRequest("GET", "/v1/sys/internal/ui/feature-flags") + httpReq, err := req.ToHTTP() + if err != nil { + t.Fatal(err) + } + resp, err := httpClient.Do(httpReq) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + + httpRespBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + + httpResp := make(map[string]interface{}) + err = json.Unmarshal(httpRespBody, &httpResp) + if err != nil { + t.Fatal(err) + } + return httpResp + } + + // First try with no environment variable set + httpResp := callApi() + featureFlags, ok := httpResp["feature_flags"] + if !ok { + t.Fatal("Missing 'feature_flags' in response") + } + if featureFlags != nil { + t.Fatal("Nonempty 'feature_flags'") + } + + // Now try with the environment variable temporarily set + envVar := "VAULT_CLOUD_ADMIN_NAMESPACE" + os.Setenv(envVar, "1") + defer os.Unsetenv(envVar) + + httpResp = callApi() + featureFlags, ok = httpResp["feature_flags"] + if !ok { + t.Fatal("Missing 'feature_flags' in response") + } + flagList := featureFlags.([]interface{}) + if len(flagList) != 1 { + t.Fatalf("Bad length for 'feature_flags': %v", flagList) + } + flag := flagList[0].(string) + if flag != envVar { + t.Fatalf("Bad environment variable in `feature_flags`: %q", flag) + } +} diff --git a/vault/logical_system.go b/vault/logical_system.go index 29076a880..c46257bac 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -4367,6 +4367,10 @@ This path responds to the following HTTP methods. "Write, Read, and Delete data directly in the Storage backend.", "", }, + "internal-ui-feature-flags": { + "Enabled feature flags. Internal API; its location, inputs, and outputs may change.", + "", + }, "internal-ui-mounts": { "Information about mounts returned according to their tuned visibility. Internal API; its location, inputs, and outputs may change.", "", diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index b47ab548b..dca220666 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -829,6 +829,17 @@ func (b *SystemBackend) internalPaths() []*framework.Path { }, }, }, + { + Pattern: "internal/ui/feature-flags", + Operations: map[logical.Operation]framework.OperationHandler{ + logical.ReadOperation: &framework.PathOperation{ + // callback is absent because this is an unauthenticated method + Summary: "Lists enabled feature flags.", + }, + }, + HelpSynopsis: strings.TrimSpace(sysHelp["internal-ui-feature-flags"][0]), + HelpDescription: strings.TrimSpace(sysHelp["internal-ui-feature-flags"][1]), + }, { Pattern: "internal/ui/mounts", Operations: map[logical.Operation]framework.OperationHandler{ diff --git a/website/content/api-docs/system/internal-ui-feature.mdx b/website/content/api-docs/system/internal-ui-feature.mdx new file mode 100644 index 000000000..23562d103 --- /dev/null +++ b/website/content/api-docs/system/internal-ui-feature.mdx @@ -0,0 +1,39 @@ +--- +layout: api +page_title: /sys/internal/ui/feature-flags - HTTP API +sidebar_title: /sys/internal/ui/feature-flags +description: >- + The `/sys/internal/ui/feature-flags` endpoint exposes feature flags to the UI. +--- + +# `/sys/internal/ui/feature-flags` + +The `/sys/internal/ui/feature-flags` endpoint is used to expose feature flags +to the UI so that it can change its behavior in response, even before a user logs in. + +This is currently only being used internally for the UI and is +an unauthenticated endpoint. Due to the nature of its intended usage, there is no +guarantee on backwards compatibility for this endpoint. + +## Get Enabled Feature Flags + +This endpoint lists the enabled feature flags relevant to the UI. + +| Method | Path | +| :----- | :------------------------------- | +| `GET` | `/sys/internal/ui/feature-flags` | + +### Sample Request + +```shell-session +$ curl \ + http://127.0.0.1:8200/v1/sys/internal/ui/feature-flags +``` + +### Sample Response + +```json +{ + "feature-flags": [] +} +``` diff --git a/website/data/api-navigation.js b/website/data/api-navigation.js index 20b1ba614..c59c6544b 100644 --- a/website/data/api-navigation.js +++ b/website/data/api-navigation.js @@ -111,6 +111,7 @@ export default [ 'init', 'internal-counters', 'internal-specs-openapi', + 'internal-ui-feature', 'internal-ui-mounts', 'key-status', 'leader',