diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 25e04d9fc5..12c7fd7837 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,79 +109,78 @@ jobs: checkout-ref: ${{ needs.setup.outputs.checkout-ref }} secrets: inherit -# test-ui: -# name: Test UI -# # The test-ui job is only run on: -# # - pushes to main and branches starting with "release/" -# # - PRs where the branch starts with "ui/", "backport/ui/", "merge", or when base branch starts with "release/" -# # - PRs with the "ui" label on GitHub -# if: | -# github.ref_name == 'main' || -# startsWith(github.ref_name, 'release/') || -# startsWith(github.head_ref, 'ui/') || -# startsWith(github.head_ref, 'backport/ui/') || -# startsWith(github.head_ref, 'merge') || -# contains(github.event.pull_request.labels.*.name, 'ui') -# needs: -# - setup -# runs-on: ubuntu-latest -# steps: -# - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 -# - uses: ./.github/actions/set-up-go -# # Setup node.js without caching to allow running npm install -g yarn (next step) -# - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 -# with: -# node-version-file: "./ui/package.json" -# - id: install-yarn -# run: | -# npm install -g yarn -# # Setup node.js with caching using the yarn.lock file -# - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 -# with: -# node-version-file: "./ui/package.json" -# cache: yarn -# cache-dependency-path: ui/yarn.lock -# - id: install-browser -# uses: browser-actions/setup-chrome@db1b524c26f20a8d1a10f7fc385c92387e2d0477 # v1.7.1 -# - id: ui-dependencies -# name: ui-dependencies -# working-directory: ./ui -# run: | -# yarn install --frozen-lockfile -# npm rebuild node-sass -# - id: build-go-dev -# name: build-go-dev -# run: | -# rm -rf ./pkg -# mkdir ./pkg -# -# make ci-bootstrap dev -# - id: test-ui -# name: test-ui -# run: | -# export PATH="${PWD}/bin:${PATH}" -# -# # Run Ember tests -# cd ui -# mkdir -p test-results/qunit -# yarn test:oss -# - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 -# with: -# name: test-results-ui -# path: ui/test-results -# if: success() || failure() -# - uses: test-summary/action@31493c76ec9e7aa675f1585d3ed6f1da69269a86 # TSCCR: no entry for repository "test-summary/action" -# with: -# paths: "ui/test-results/qunit/results.xml" -# show: "fail" -# if: always() + test-ui: + name: Test UI + # The test-ui job is only run on: + # - pushes to main and branches starting with "release/" + # - PRs where the branch starts with "ui/", "backport/ui/", "merge", or when base branch starts with "release/" + # - PRs with the "ui" label on GitHub + if: | + github.ref_name == 'main' || + startsWith(github.ref_name, 'release/') || + startsWith(github.head_ref, 'ui/') || + startsWith(github.head_ref, 'backport/ui/') || + startsWith(github.head_ref, 'merge') || + contains(github.event.pull_request.labels.*.name, 'ui') + needs: + - setup + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + - uses: ./.github/actions/set-up-go + # Setup node.js without caching to allow running npm install -g yarn (next step) + - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + with: + node-version-file: "./ui/package.json" + - id: install-yarn + run: | + npm install -g yarn + # Setup node.js with caching using the yarn.lock file + - uses: actions/setup-node@e33196f7422957bea03ed53f6fbb155025ffc7b8 # v3.7.0 + with: + node-version-file: "./ui/package.json" + cache: yarn + cache-dependency-path: ui/yarn.lock + - id: install-browser + uses: browser-actions/setup-chrome@db1b524c26f20a8d1a10f7fc385c92387e2d0477 # v1.7.1 + - id: ui-dependencies + name: ui-dependencies + working-directory: ./ui + run: | + yarn install --frozen-lockfile + npm rebuild node-sass + - id: build-go-dev + name: build-go-dev + run: | + rm -rf ./pkg + mkdir ./pkg + + make ci-bootstrap dev + - id: test-ui + name: test-ui + run: | + export PATH="${PWD}/bin:${PATH}" + + # Run Ember tests + cd ui + mkdir -p test-results/qunit + yarn test:oss + - uses: actions/upload-artifact@834a144ee995460fba8ed112a2fc961b36a5ec5a # v4.3.6 + with: + name: test-results-ui + path: ui/test-results + if: success() || failure() + - uses: test-summary/action@31493c76ec9e7aa675f1585d3ed6f1da69269a86 # TSCCR: no entry for repository "test-summary/action" + with: + paths: "ui/test-results/qunit/results.xml" + show: "fail" + if: always() tests-completed: needs: - setup - test-go - # UI testing is currently disabled. - # - test-ui + - test-ui if: always() runs-on: ubuntu-latest steps: diff --git a/api/logical.go b/api/logical.go index 8727396fcb..4bc258f3f8 100644 --- a/api/logical.go +++ b/api/logical.go @@ -239,6 +239,86 @@ func (c *Logical) ListPageWithContext(ctx context.Context, path string, after st return ParseSecret(resp.Body) } +func (c *Logical) Scan(path string) (*Secret, error) { + return c.ScanWithContext(context.Background(), path) +} + +func (c *Logical) ScanWithContext(ctx context.Context, path string) (*Secret, error) { + ctx, cancelFunc := c.c.withConfiguredTimeout(ctx) + defer cancelFunc() + + r := c.c.NewRequest("SCAN", "/v1/"+path) + // Set this for broader compatibility, but we use SCAN above to be able to + // handle the wrapping lookup function + r.Method = http.MethodGet + r.Params.Set("scan", "true") + + resp, err := c.c.rawRequestWithContext(ctx, r) + if resp != nil { + defer resp.Body.Close() + } + if resp != nil && resp.StatusCode == 404 { + secret, parseErr := ParseSecret(resp.Body) + switch parseErr { + case nil: + case io.EOF: + return nil, nil + default: + return nil, parseErr + } + if secret != nil && (len(secret.Warnings) > 0 || len(secret.Data) > 0) { + return secret, nil + } + return nil, nil + } + if err != nil { + return nil, err + } + + return ParseSecret(resp.Body) +} + +func (c *Logical) ScanPage(path string, after string, limit int) (*Secret, error) { + return c.ScanPageWithContext(context.Background(), path, after, limit) +} + +func (c *Logical) ScanPageWithContext(ctx context.Context, path string, after string, limit int) (*Secret, error) { + ctx, cancelFunc := c.c.withConfiguredTimeout(ctx) + defer cancelFunc() + + r := c.c.NewRequest("SCAN", "/v1/"+path) + // Set this for broader compatibility, but we use SCAN above to be able to + // handle the wrapping lookup function. + r.Method = http.MethodGet + r.Params.Set("scan", "true") + r.Params.Set("after", after) + r.Params.Set("limit", fmt.Sprintf("%d", limit)) + + resp, err := c.c.rawRequestWithContext(ctx, r) + if resp != nil { + defer resp.Body.Close() + } + if resp != nil && resp.StatusCode == 404 { + secret, parseErr := ParseSecret(resp.Body) + switch parseErr { + case nil: + case io.EOF: + return nil, nil + default: + return nil, parseErr + } + if secret != nil && (len(secret.Warnings) > 0 || len(secret.Data) > 0) { + return secret, nil + } + return nil, nil + } + if err != nil { + return nil, err + } + + return ParseSecret(resp.Body) +} + func (c *Logical) Write(path string, data map[string]interface{}) (*Secret, error) { return c.WriteWithContext(context.Background(), path, data) } diff --git a/builtin/logical/kv/backend.go b/builtin/logical/kv/backend.go index e29a87ef95..d40729aea4 100644 --- a/builtin/logical/kv/backend.go +++ b/builtin/logical/kv/backend.go @@ -182,6 +182,8 @@ func pathInvalid(b *versionedKVBackend) []*framework.Path { subCommand = "get" case logical.ListOperation: subCommand = "list" + case logical.ScanOperation: + subCommand = "scan" case logical.DeleteOperation: subCommand = "delete" } @@ -200,6 +202,7 @@ func pathInvalid(b *versionedKVBackend) []*framework.Path { logical.ReadOperation: &framework.PathOperation{Callback: handler, Unpublished: true}, logical.DeleteOperation: &framework.PathOperation{Callback: handler, Unpublished: true}, logical.ListOperation: &framework.PathOperation{Callback: handler, Unpublished: true}, + logical.ScanOperation: &framework.PathOperation{Callback: handler, Unpublished: true}, }, HelpDescription: pathInvalidHelp, diff --git a/builtin/logical/kv/passthrough.go b/builtin/logical/kv/passthrough.go index a7b8b4593e..14abcdbf87 100644 --- a/builtin/logical/kv/passthrough.go +++ b/builtin/logical/kv/passthrough.go @@ -69,6 +69,7 @@ func LeaseSwitchedPassthroughBackend(ctx context.Context, conf *logical.BackendC logical.UpdateOperation: b.handleWrite(), logical.DeleteOperation: b.handleDelete(), logical.ListOperation: b.handleList(), + logical.ScanOperation: b.handleScan(), }, ExistenceCheck: b.handleExistenceCheck(), @@ -256,6 +257,30 @@ func (b *PassthroughBackend) handleList() framework.OperationFunc { } } +func (b *PassthroughBackend) handleScan() framework.OperationFunc { + return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + // Right now we only handle directories, so ensure it ends with /; however, + // some physical backends may not handle the "/" case properly, so only add + // it if we're not listing the root + path := data.Get("path").(string) + if path != "" && !strings.HasSuffix(path, "/") { + path = path + "/" + } + + // List the keys at the prefix given by the request + var keys []string + err := logical.ScanView(ctx, logical.NewStorageView(req.Storage, path), func(p string) { + keys = append(keys, p) + }) + if err != nil { + return nil, err + } + + // Generate the response + return logical.ListResponse(keys), nil + } +} + const passthroughHelp = ` The kv backend reads and writes arbitrary secrets to the backend. The secrets are encrypted/decrypted by Vault: they are never stored diff --git a/builtin/logical/kv/passthrough_test.go b/builtin/logical/kv/passthrough_test.go index e05291fe41..ee5e2459d6 100644 --- a/builtin/logical/kv/passthrough_test.go +++ b/builtin/logical/kv/passthrough_test.go @@ -210,6 +210,39 @@ func TestPassthroughBackend_List(t *testing.T) { test(b) } +func TestPassthroughBackend_Scan(t *testing.T) { + test := func(b logical.Backend) { + req := logical.TestRequest(t, logical.UpdateOperation, "foo") + req.Data["raw"] = "test" + storage := req.Storage + + if _, err := b.HandleRequest(context.Background(), req); err != nil { + t.Fatalf("err: %v", err) + } + + req = logical.TestRequest(t, logical.ScanOperation, "") + req.Storage = storage + resp, err := b.HandleRequest(context.Background(), req) + if err != nil { + t.Fatalf("err: %v", err) + } + + expected := &logical.Response{ + Data: map[string]interface{}{ + "keys": []string{"foo"}, + }, + } + + if !reflect.DeepEqual(resp, expected) { + t.Fatalf("bad response.\n\nexpected: %#v\n\nGot: %#v", expected, resp) + } + } + b := testPassthroughBackend() + test(b) + b = testPassthroughLeasedBackend() + test(b) +} + func TestPassthroughBackend_Revoke(t *testing.T) { test := func(b logical.Backend) { req := logical.TestRequest(t, logical.RevokeOperation, "kv") diff --git a/builtin/logical/kv/path_metadata.go b/builtin/logical/kv/path_metadata.go index b0f8c596d5..3a1e463507 100644 --- a/builtin/logical/kv/path_metadata.go +++ b/builtin/logical/kv/path_metadata.go @@ -71,6 +71,7 @@ version-agnostic information about a secret. logical.ReadOperation: b.upgradeCheck(b.pathMetadataRead()), logical.DeleteOperation: b.upgradeCheck(b.pathMetadataDelete()), logical.ListOperation: b.upgradeCheck(b.pathMetadataList()), + logical.ScanOperation: b.upgradeCheck(b.pathMetadataScan()), logical.PatchOperation: b.upgradeCheck(b.pathMetadataPatch()), }, @@ -125,6 +126,32 @@ func (b *versionedKVBackend) pathMetadataList() framework.OperationFunc { } } +func (b *versionedKVBackend) pathMetadataScan() framework.OperationFunc { + return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + key := data.Get("path").(string) + + // Get an encrypted key storage object + wrapper, err := b.getKeyEncryptor(ctx, req.Storage) + if err != nil { + return nil, err + } + + es := wrapper.Wrap(req.Storage) + view := logical.NewStorageView(es, key) + + // Use encrypted key storage to recursively list the keys + var keys []string + err = logical.ScanView(ctx, view, func(path string) { + keys = append(keys, path) + }) + if err != nil { + return nil, err + } + + return logical.ListResponse(keys), nil + } +} + func (b *versionedKVBackend) pathMetadataRead() framework.OperationFunc { return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { // Create a read-only transaction if we can. We do not need to commit diff --git a/changelog/763.txt b/changelog/763.txt new file mode 100644 index 0000000000..8293ebd143 --- /dev/null +++ b/changelog/763.txt @@ -0,0 +1,3 @@ +```release-note:feature +**Scanning**: introduce the ability to recursively list (scan) within plugins, adding a separate `scan` ACL capability, operation type, HTTP verb (`SCAN` with `GET` fallback via `?scan=true`), API, and CLI support. This also adds support to the KVv1 and KVv2 engines. +``` diff --git a/command/commands.go b/command/commands.go index 727e08bb2b..ba9f1d3214 100644 --- a/command/commands.go +++ b/command/commands.go @@ -578,6 +578,11 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) map[string]cli.Co BaseCommand: getBaseCommand(), }, nil }, + "scan": func() (cli.Command, error) { + return &ScanCommand{ + BaseCommand: getBaseCommand(), + }, nil + }, "secrets": func() (cli.Command, error) { return &SecretsCommand{ BaseCommand: getBaseCommand(), @@ -778,6 +783,11 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) map[string]cli.Co BaseCommand: getBaseCommand(), }, nil }, + "kv scan": func() (cli.Command, error) { + return &KVScanCommand{ + BaseCommand: getBaseCommand(), + }, nil + }, "monitor": func() (cli.Command, error) { return &MonitorCommand{ BaseCommand: getBaseCommand(), diff --git a/command/kv_scan.go b/command/kv_scan.go new file mode 100644 index 0000000000..b326b1ce72 --- /dev/null +++ b/command/kv_scan.go @@ -0,0 +1,179 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package command + +import ( + "fmt" + "path" + "strings" + + "github.com/hashicorp/cli" + "github.com/posener/complete" +) + +var ( + _ cli.Command = (*KVScanCommand)(nil) + _ cli.CommandAutocomplete = (*KVScanCommand)(nil) +) + +type KVScanCommand struct { + *BaseCommand + flagMount string +} + +func (c *KVScanCommand) Synopsis() string { + return "Scan data or secrets" +} + +func (c *KVScanCommand) Help() string { + helpText := ` + +Usage: bao kv scan [options] PATH + + Scans data from OpenBao's key-value store at the given path. + + Scan values under the "my-app" folder of the key-value store: + + $ bao kv scan secret/my-app/ + + Additional flags and more advanced use cases are detailed below. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *KVScanCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat) + + // Common Options + f := set.NewFlagSet("Common Options") + + f.StringVar(&StringVar{ + Name: "mount", + Target: &c.flagMount, + Default: "", // no default, because the handling of the next arg is determined by whether this flag has a value + Usage: `Specifies the path where the KV backend is mounted. If specified, + the next argument will be interpreted as the secret path. If this flag is + not specified, the next argument will be interpreted as the combined mount + path and secret path, with /data/ automatically appended between KV + v2 secrets.`, + }) + + return set +} + +func (c *KVScanCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultFolders() +} + +func (c *KVScanCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *KVScanCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + switch { + case len(args) < 1: + if c.flagMount == "" { + c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args))) + return 1 + } + args = []string{""} + case len(args) > 1: + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + // If true, we're working with "-mount=secret foo" syntax. + // If false, we're using "secret/foo" syntax. + mountFlagSyntax := c.flagMount != "" + + var ( + mountPath string + partialPath string + v2 bool + ) + + // Parse the paths and grab the KV version + if mountFlagSyntax { + // In this case, this arg is the secret path (e.g. "foo"). + partialPath = sanitizePath(args[0]) + mountPath, v2, err = isKVv2(sanitizePath(c.flagMount), client) + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + if v2 { + partialPath = path.Join(mountPath, partialPath) + } + } else { + // In this case, this arg is a path-like combination of mountPath/secretPath. + // (e.g. "secret/foo") + partialPath = sanitizePath(args[0]) + mountPath, v2, err = isKVv2(partialPath, client) + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + } + + // Add /metadata to v2 paths only + var fullPath string + if v2 { + fullPath = addPrefixToKVPath(partialPath, mountPath, "metadata", false) + } else { + // v1 + if mountFlagSyntax { + fullPath = path.Join(mountPath, partialPath) + } else { + fullPath = partialPath + } + } + + secret, err := client.Logical().Scan(fullPath) + if err != nil { + c.UI.Error(fmt.Sprintf("Error scanning %s: %s", fullPath, err)) + return 2 + } + + // If the secret is wrapped, return the wrapped response. + if secret != nil && secret.WrapInfo != nil && secret.WrapInfo.TTL != 0 { + return OutputSecret(c.UI, secret) + } + + _, ok := extractListData(secret) + if Format(c.UI) != "table" { + if secret == nil || secret.Data == nil || !ok { + OutputData(c.UI, map[string]interface{}{}) + return 2 + } + } + + if secret == nil || secret.Data == nil { + c.UI.Error(fmt.Sprintf("No value found at %s", fullPath)) + return 2 + } + + if !ok { + c.UI.Error(fmt.Sprintf("No entries found at %s", fullPath)) + return 2 + } + + return OutputList(c.UI, secret) +} diff --git a/command/scan.go b/command/scan.go new file mode 100644 index 0000000000..e213c1c0e9 --- /dev/null +++ b/command/scan.go @@ -0,0 +1,161 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package command + +import ( + "fmt" + "strings" + + "github.com/hashicorp/cli" + "github.com/openbao/openbao/api/v2" + "github.com/posener/complete" +) + +var ( + _ cli.Command = (*ScanCommand)(nil) + _ cli.CommandAutocomplete = (*ScanCommand)(nil) +) + +type ScanCommand struct { + *BaseCommand + + flagAfter string + flagLimit int +} + +func (c *ScanCommand) Synopsis() string { + return "Scan (recursively list) data or secrets" +} + +func (c *ScanCommand) Help() string { + helpText := ` + +Usage: bao scan [options] PATH + + Scans data from Vault at the given path. This can be used to scan keys in a + given secret engine. Scanning amounts to a recursive listing on all entries. + + Scan values under the "my-app" folder of the generic secret engine: + + $ bao scan secret/my-app/ + + Some paths support paginated scanning. Use the -after and -limit flags to + control the return of data: + + $ bao scan -after=last-serial -limit=50 pki/certs + + Some paths may support returning additional information about items; + use the -detailed flag to see this info: + + $ bao scan -detailed secret/detailed-metadata/foo + + For a full list of examples and paths, please see the documentation that + corresponds to the secret engine in use. Not all engines support scanning. + +` + c.Flags().Help() + + return strings.TrimSpace(helpText) +} + +func (c *ScanCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat | FlagSetOutputDetailed) + + f := set.NewFlagSet("Command Options") + + f.StringVar(&StringVar{ + Name: "after", + Target: &c.flagAfter, + Default: "", + Usage: "Last seen key on applicable endpoints; the next key" + + "alphabetically will be the first returned.", + }) + + f.IntVar(&IntVar{ + Name: "limit", + Target: &c.flagLimit, + Default: -1, + Usage: "Limits the number of scan responses on applicable endpoints.", + }) + + return set +} + +func (c *ScanCommand) AutocompleteArgs() complete.Predictor { + return c.PredictVaultFolders() +} + +func (c *ScanCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *ScanCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + args = f.Args() + switch { + case len(args) < 1: + c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args))) + return 1 + case len(args) > 1: + c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args))) + return 1 + } + + client, err := c.Client() + if err != nil { + c.UI.Error(err.Error()) + return 2 + } + + path := sanitizePath(args[0]) + + // Only dispatch a ScanPage operation if flags were given; this avoids + // a warning from the server about unrecognized parameters if the scan + // endpoint doesn't understand pagination. + var secret *api.Secret + if c.flagAfter == "" && c.flagLimit <= 0 { + secret, err = client.Logical().Scan(path) + } else { + secret, err = client.Logical().ScanPage(path, c.flagAfter, c.flagLimit) + } + if err != nil { + c.UI.Error(fmt.Sprintf("Error scanning %s: %s", path, err)) + return 2 + } + + // If the secret is wrapped, return the wrapped response. + if secret != nil && secret.WrapInfo != nil && secret.WrapInfo.TTL != 0 { + return OutputSecret(c.UI, secret) + } + + _, ok := extractListData(secret) + if Format(c.UI) != "table" { + if secret == nil || secret.Data == nil || !ok { + OutputData(c.UI, map[string]interface{}{}) + return 2 + } + } + + if secret == nil { + c.UI.Error(fmt.Sprintf("No value found at %s", path)) + return 2 + } + if secret.Data == nil { + // If secret wasn't nil, we have warnings, so output them anyways. We + // may also have non-keys info. + return OutputSecret(c.UI, secret) + } + + if !ok { + c.UI.Error(fmt.Sprintf("No entries found at %s", path)) + return 2 + } + + return OutputList(c.UI, secret) +} diff --git a/command/scan_test.go b/command/scan_test.go new file mode 100644 index 0000000000..1c1d062cfd --- /dev/null +++ b/command/scan_test.go @@ -0,0 +1,135 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package command + +import ( + "strings" + "testing" + + "github.com/hashicorp/cli" +) + +func testScanCommand(tb testing.TB) (*cli.MockUi, *ScanCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &ScanCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + } +} + +func TestScanCommand_Run(t *testing.T) { + t.Parallel() + + cases := []struct { + name string + args []string + out string + code int + }{ + { + "not_enough_args", + []string{}, + "Not enough arguments", + 1, + }, + { + "too_many_args", + []string{"foo", "bar"}, + "Too many arguments", + 1, + }, + { + "not_found", + []string{"nope/not/once/never"}, + "", + 2, + }, + { + "default", + []string{"secret/scan"}, + "bar\nbaz\nfoo", + 0, + }, + { + "default_slash", + []string{"secret/scan/"}, + "bar\nbaz\nfoo", + 0, + }, + } + + t.Run("validations", func(t *testing.T) { + t.Parallel() + + for _, tc := range cases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServer(t) + defer closer() + + keys := []string{ + "secret/scan/foo", + "secret/scan/bar", + "secret/scan/baz", + } + for _, k := range keys { + if _, err := client.Logical().Write(k, map[string]interface{}{ + "foo": "bar", + }); err != nil { + t.Fatal(err) + } + } + + ui, cmd := testScanCommand(t) + cmd.client = client + + code := cmd.Run(tc.args) + if code != tc.code { + t.Errorf("expected %d to be %d", code, tc.code) + } + + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, tc.out) { + t.Errorf("expected %q to contain %q", combined, tc.out) + } + }) + } + }) + + t.Run("communication_failure", func(t *testing.T) { + t.Parallel() + + client, closer := testVaultServerBad(t) + defer closer() + + ui, cmd := testScanCommand(t) + cmd.client = client + + code := cmd.Run([]string{ + "secret/scan", + }) + if exp := 2; code != exp { + t.Errorf("expected %d to be %d", code, exp) + } + + expected := "Error scanning secret/scan: " + combined := ui.OutputWriter.String() + ui.ErrorWriter.String() + if !strings.Contains(combined, expected) { + t.Errorf("expected %q to contain %q", combined, expected) + } + }) + + t.Run("no_tabs", func(t *testing.T) { + t.Parallel() + + _, cmd := testScanCommand(t) + assertNoTabs(t, cmd) + }) +} diff --git a/http/cors.go b/http/cors.go index 2eaae87220..d9412535fc 100644 --- a/http/cors.go +++ b/http/cors.go @@ -19,6 +19,7 @@ var allowedMethods = []string{ http.MethodPost, http.MethodPut, "LIST", // LIST is not an official HTTP method, but Vault supports it. + "SCAN", // SCAN is not an official HTTP method, but Vault supports it. } func wrapCORSHandler(h http.Handler, core *vault.Core) http.Handler { diff --git a/http/logical.go b/http/logical.go index e59bdf7d6d..1ab1ede806 100644 --- a/http/logical.go +++ b/http/logical.go @@ -65,20 +65,42 @@ func buildLogicalRequestNoAuth(w http.ResponseWriter, r *http.Request) (*logical case "GET": op = logical.ReadOperation queryVals := r.URL.Query() + var list bool + var scan bool var err error + listStr := queryVals.Get("list") if listStr != "" { list, err = strconv.ParseBool(listStr) if err != nil { return nil, nil, http.StatusBadRequest, nil } - if list { - queryVals.Del("list") - op = logical.ListOperation - if !strings.HasSuffix(path, "/") { - path += "/" - } + } + + scanStr := queryVals.Get("scan") + if scanStr != "" { + scan, err = strconv.ParseBool(scanStr) + if err != nil { + return nil, nil, http.StatusBadRequest, nil + } + } + + if list && scan { + return nil, nil, http.StatusBadRequest, nil + } + + if list { + queryVals.Del("list") + op = logical.ListOperation + if !strings.HasSuffix(path, "/") { + path += "/" + } + } else if scan { + queryVals.Del("scan") + op = logical.ScanOperation + if !strings.HasSuffix(path, "/") { + path += "/" } } @@ -179,6 +201,13 @@ func buildLogicalRequestNoAuth(w http.ResponseWriter, r *http.Request) (*logical path += "/" } + data = parseQuery(r.URL.Query()) + case "SCAN": + op = logical.ScanOperation + if !strings.HasSuffix(path, "/") { + path += "/" + } + data = parseQuery(r.URL.Query()) case "HEAD": op = logical.HeaderOperation @@ -233,6 +262,7 @@ func buildLogicalPath(r *http.Request) (string, int, error) { case "GET": var ( list bool + scan bool err error ) @@ -244,17 +274,33 @@ func buildLogicalPath(r *http.Request) (string, int, error) { if err != nil { return "", http.StatusBadRequest, nil } - if list { - if !strings.HasSuffix(path, "/") { - path += "/" - } + } + + scanStr := queryVals.Get("scan") + if scanStr != "" { + scan, err = strconv.ParseBool(scanStr) + if err != nil { + return "", http.StatusBadRequest, nil } } + if list && scan { + return "", http.StatusBadRequest, nil + } + + if list || scan { + if !strings.HasSuffix(path, "/") { + path += "/" + } + } case "LIST": if !strings.HasSuffix(path, "/") { path += "/" } + case "SCAN": + if !strings.HasSuffix(path, "/") { + path += "/" + } } return path, 0, nil diff --git a/http/logical_test.go b/http/logical_test.go index 6f454a6f3d..43e9096bb2 100644 --- a/http/logical_test.go +++ b/http/logical_test.go @@ -443,6 +443,138 @@ func TestLogical_ListWithQueryParameters(t *testing.T) { } } +func TestLogical_ScanSuffix(t *testing.T) { + core, _, rootToken := vault.TestCoreUnsealed(t) + req, _ := http.NewRequest("GET", "http://127.0.0.1:8200/v1/secret/foo", nil) + req = req.WithContext(namespace.RootContext(nil)) + req.Header.Add(consts.AuthHeaderName, rootToken) + + lreq, _, status, err := buildLogicalRequest(core, nil, req) + if err != nil { + t.Fatal(err) + } + if status != 0 { + t.Fatalf("got status %d", status) + } + if strings.HasSuffix(lreq.Path, "/") { + t.Fatal("trailing slash found on path") + } + + req, _ = http.NewRequest("GET", "http://127.0.0.1:8200/v1/secret/foo?scan=true", nil) + req = req.WithContext(namespace.RootContext(nil)) + req.Header.Add(consts.AuthHeaderName, rootToken) + + lreq, _, status, err = buildLogicalRequest(core, nil, req) + if err != nil { + t.Fatal(err) + } + if status != 0 { + t.Fatalf("got status %d", status) + } + if !strings.HasSuffix(lreq.Path, "/") { + t.Fatal("trailing slash not found on path") + } + + req, _ = http.NewRequest("SCAN", "http://127.0.0.1:8200/v1/secret/foo", nil) + req = req.WithContext(namespace.RootContext(nil)) + req.Header.Add(consts.AuthHeaderName, rootToken) + + _, _, status, err = buildLogicalRequestNoAuth(nil, req) + if err != nil || status != 0 { + t.Fatal(err) + } + + lreq, _, status, err = buildLogicalRequest(core, nil, req) + if err != nil { + t.Fatal(err) + } + if status != 0 { + t.Fatalf("got status %d", status) + } + if !strings.HasSuffix(lreq.Path, "/") { + t.Fatal("trailing slash not found on path") + } +} + +func TestLogical_ScanWithQueryParameters(t *testing.T) { + core, _, rootToken := vault.TestCoreUnsealed(t) + + tests := []struct { + name string + requestMethod string + url string + expectedData map[string]interface{} + }{ + { + name: "SCAN request method parses query parameter", + requestMethod: "SCAN", + url: "http://127.0.0.1:8200/v1/secret/foo?key1=value1", + expectedData: map[string]interface{}{ + "key1": "value1", + }, + }, + { + name: "SCAN request method parses query multiple parameters", + requestMethod: "SCAN", + url: "http://127.0.0.1:8200/v1/secret/foo?key1=value1&key2=value2", + expectedData: map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }, + }, + { + name: "GET request method with scan=true parses query parameter", + requestMethod: "GET", + url: "http://127.0.0.1:8200/v1/secret/foo?scan=true&key1=value1", + expectedData: map[string]interface{}{ + "key1": "value1", + }, + }, + { + name: "GET request method with scan=true parses multiple query parameters", + requestMethod: "GET", + url: "http://127.0.0.1:8200/v1/secret/foo?scan=true&key1=value1&key2=value2", + expectedData: map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }, + }, + { + name: "GET request method with alternate order scan=true parses multiple query parameters", + requestMethod: "GET", + url: "http://127.0.0.1:8200/v1/secret/foo?key1=value1&scan=true&key2=value2", + expectedData: map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + req, _ := http.NewRequest(tc.requestMethod, tc.url, nil) + req = req.WithContext(namespace.RootContext(nil)) + req.Header.Add(consts.AuthHeaderName, rootToken) + + lreq, _, status, err := buildLogicalRequest(core, nil, req) + if err != nil { + t.Fatal(err) + } + if status != 0 { + t.Fatalf("got status %d", status) + } + if !strings.HasSuffix(lreq.Path, "/") { + t.Fatal("trailing slash not found on path") + } + if lreq.Operation != logical.ScanOperation { + t.Fatalf("expected logical.ScanOperation, got %v", lreq.Operation) + } + if !reflect.DeepEqual(tc.expectedData, lreq.Data) { + t.Fatalf("expected query parameter data %v, got %v", tc.expectedData, lreq.Data) + } + }) + } +} + func TestLogical_RespondWithStatusCode(t *testing.T) { resp := &logical.Response{ Data: map[string]interface{}{ diff --git a/sdk/framework/openapi.go b/sdk/framework/openapi.go index 0862461c14..fd5bdedc16 100644 --- a/sdk/framework/openapi.go +++ b/sdk/framework/openapi.go @@ -418,6 +418,15 @@ func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix st In: "query", Schema: &OASSchema{Type: "string", Enum: []interface{}{"true"}}, }) + } else if opType == logical.ScanOperation { + // Only accepts List (due to the above skipping of ListOperations that also have ReadOperations) + op.Parameters = append(op.Parameters, OASParameter{ + Name: "scan", + Description: "Must be set to `true`", + Required: true, + In: "query", + Schema: &OASSchema{Type: "string", Enum: []interface{}{"true"}}, + }) } else if opType == logical.ReadOperation && operations[logical.ListOperation] != nil { // Accepts both Read and List op.Parameters = append(op.Parameters, OASParameter{ @@ -426,6 +435,14 @@ func documentPath(p *Path, specialPaths *logical.Paths, requestResponsePrefix st In: "query", Schema: &OASSchema{Type: "string"}, }) + } else if opType == logical.ReadOperation && operations[logical.ScanOperation] != nil { + // Accepts both Read and List + op.Parameters = append(op.Parameters, OASParameter{ + Name: "scan", + Description: "Return a recursive list if `true`", + In: "query", + Schema: &OASSchema{Type: "string"}, + }) } // Add tags based on backend type diff --git a/sdk/helper/stepwise/stepwise.go b/sdk/helper/stepwise/stepwise.go index 752e47473b..63c8c56d38 100644 --- a/sdk/helper/stepwise/stepwise.go +++ b/sdk/helper/stepwise/stepwise.go @@ -28,6 +28,7 @@ const ( ReadOperation = "read" DeleteOperation = "delete" ListOperation = "list" + ScanOperation = "scan" HelpOperation = "help" ) diff --git a/sdk/logical/request.go b/sdk/logical/request.go index cf289f5516..a9af6bc346 100644 --- a/sdk/logical/request.go +++ b/sdk/logical/request.go @@ -356,6 +356,7 @@ const ( PatchOperation = "patch" DeleteOperation = "delete" ListOperation = "list" + ScanOperation = "scan" HelpOperation = "help" AliasLookaheadOperation = "alias-lookahead" ResolveRoleOperation = "resolve-role" diff --git a/ui/Makefile b/ui/Makefile index 821db2ead0..366e89a2dd 100644 --- a/ui/Makefile +++ b/ui/Makefile @@ -4,7 +4,7 @@ frontend: .PHONY: backend backend: - yarn bao + yarn openbao .PHONY: test test: diff --git a/ui/app/styles/components/console-ui-panel.scss b/ui/app/styles/components/console-ui-panel.scss index 3338726ee2..783298c376 100644 --- a/ui/app/styles/components/console-ui-panel.scss +++ b/ui/app/styles/components/console-ui-panel.scss @@ -8,7 +8,7 @@ $console-close-height: 35px; .console-ui-panel { background: var(--token-color-palette-neutral-700); width: -moz-available; - width: -webkit-fill-available; + width: -webkit-stretch; height: 0; min-height: 0; overflow: auto; diff --git a/ui/app/styles/core/checkbox-and-radio.scss b/ui/app/styles/core/checkbox-and-radio.scss index bdf678e276..04af2a5e2b 100644 --- a/ui/app/styles/core/checkbox-and-radio.scss +++ b/ui/app/styles/core/checkbox-and-radio.scss @@ -5,7 +5,7 @@ // This file defines the styles for .checkbox, .radio and .b-checkboxes. The prefix "b" comes from Bulma. -@import '../sass-svg-uri/svg-uri'; +@import '../sass-svg-uri'; // checkbox and radio styling .checkbox, diff --git a/ui/package.json b/ui/package.json index 8ee1a83277..c69654a302 100644 --- a/ui/package.json +++ b/ui/package.json @@ -22,17 +22,17 @@ "fmt:js": "prettier --config .prettierrc.js --write '{app,tests,config,lib}/**/*.js'", "fmt:hbs": "prettier --config .prettierrc.js --write '**/*.hbs'", "fmt:styles": "prettier --write app/styles/**/*.*", - "start": "VAULT_ADDR=http://localhost:8200; ember server --proxy=$VAULT_ADDR", + "start": "BAO_ADDR=http://localhost:8200; ember server --proxy=$BAO_ADDR", "start2": "ember server --proxy=http://localhost:8202 --port=4202", "start:mirage": "start () { MIRAGE_DEV_HANDLER=$1 yarn run start; }; start", - "test": "npm-run-all lint:js:quiet lint:hbs:quiet && node scripts/start-vault.js", + "test": "npm-run-all lint:js:quiet lint:hbs:quiet && node scripts/start-openbao.js", "test:enos": "npm-run-all lint:js:quiet lint:hbs:quiet && node scripts/enos-test-ember.js", "test:oss": "yarn run test -f='!enterprise'", - "test:quick": "node scripts/start-vault.js", + "test:quick": "node scripts/start-openbao.js", "test:quick-oss": "yarn test:quick -f='!enterprise'", "types:declare": "declare () { yarn tsc $1 --declaration --allowJs --emitDeclarationOnly --experimentalDecorators --outDir $2; }; declare", - "vault": "VAULT_REDIRECT_ADDR=http://127.0.0.1:8200 vault server -log-level=error -dev -dev-root-token-id=root -dev-ha -dev-transactional", - "vault:cluster": "VAULT_REDIRECT_ADDR=http://127.0.0.1:8202 vault server -log-level=error -dev -dev-root-token-id=root -dev-listen-address=127.0.0.1:8202 -dev-ha -dev-transactional" + "openbao": "BAO_REDIRECT_ADDR=http://127.0.0.1:8200 bao server -log-level=error -dev -dev-root-token-id=root -dev-ha", + "openbao:cluster": "BAO_REDIRECT_ADDR=http://127.0.0.1:8202 bao server -log-level=error -dev -dev-root-token-id=root -dev-listen-address=127.0.0.1:8202 -dev-ha" }, "lint-staged": { "*.js": [ @@ -189,7 +189,7 @@ "qunit": "^2.19.1", "qunit-dom": "^2.0.0", "sass": "^1.58.3", - "sass-svg-uri": "^1.0.0", + "sass-svg-uri": "^2.0.0", "shell-quote": "^1.6.1", "string.prototype.endswith": "^0.2.0", "string.prototype.startswith": "^0.2.0", diff --git a/ui/scripts/start-vault.js b/ui/scripts/start-openbao.js similarity index 76% rename from ui/scripts/start-vault.js rename to ui/scripts/start-openbao.js index 1bcf53a5e8..6d34f16cf2 100755 --- a/ui/scripts/start-vault.js +++ b/ui/scripts/start-openbao.js @@ -28,14 +28,7 @@ async function processLines(input, eachLine = () => {}) { try { const vault = testHelper.run( 'bao', - [ - 'server', - '-dev', - '-dev-ha', - '-dev-transactional', - '-dev-root-token-id=root', - '-dev-listen-address=127.0.0.1:9200', - ], + ['server', '-dev', '-dev-ha', '-dev-root-token-id=root', '-dev-listen-address=127.0.0.1:9200'], false ); processLines(vault.stdout, function (line) { @@ -59,13 +52,9 @@ async function processLines(input, eachLine = () => {}) { if (root && unseal && !written) { testHelper.writeKeysFile(unseal, root); written = true; - console.log('VAULT SERVER READY'); + console.log('OPENBAO SERVER READY'); } else if (initError) { - console.log('VAULT SERVER START FAILED'); - console.log( - // NOTE: Remove VAULT_LICENSE_PATH reference; - 'If this is happening, run `export VAULT_LICENSE_PATH=/Users/username/license.hclic` to your valid local vault license filepath, or use OpenBao' - ); + console.log('OPENBAO SERVER START FAILED'); process.exit(1); } }); diff --git a/ui/tests/acceptance/access/namespaces/index-test.js b/ui/tests/acceptance/access/namespaces/index-test.js deleted file mode 100644 index 6a756a1888..0000000000 --- a/ui/tests/acceptance/access/namespaces/index-test.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { currentRouteName } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import page from 'vault/tests/pages/access/namespaces/index'; -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; - -module('Acceptance | Enterprise | /access/namespaces', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - hooks.afterEach(function () { - return logout.visit(); - }); - - test('it navigates to namespaces page', async function (assert) { - assert.expect(1); - await page.visit(); - assert.strictEqual( - currentRouteName(), - 'vault.cluster.access.namespaces.index', - 'navigates to the correct route' - ); - }); - - test('it should render correct number of namespaces', async function (assert) { - assert.expect(3); - await page.visit(); - const store = this.owner.lookup('service:store'); - // Default page size is 15 - assert.strictEqual(store.peekAll('namespace').length, 15, 'Store has 15 namespaces records'); - assert.dom('.list-item-row').exists({ count: 15 }); - assert.dom('[data-test-list-view-pagination]').exists(); - }); -}); diff --git a/ui/tests/acceptance/auth-list-test.js b/ui/tests/acceptance/auth-list-test.js index 89ecfed35c..f6163380fe 100644 --- a/ui/tests/acceptance/auth-list-test.js +++ b/ui/tests/acceptance/auth-list-test.js @@ -124,20 +124,4 @@ module('Acceptance | auth backend list', function (hooks) { } } }); - - test('enterprise: token config within namespace', async function (assert) { - const ns = 'ns-wxyz'; - await consoleComponent.runCommands(`write sys/namespaces/${ns} -f`); - await authPage.loginNs(ns); - // go directly to token configure route - await visit('/vault/settings/auth/configure/token/options'); - await fillIn('[data-test-input="description"]', 'My custom description'); - await click('[data-test-save-config="true"]'); - assert.strictEqual(currentURL(), '/vault/access', 'successfully saves and navigates away'); - await click('[data-test-auth-backend-link="token"]'); - assert - .dom('[data-test-row-value="Description"]') - .hasText('My custom description', 'description was saved'); - await consoleComponent.runCommands(`delete sys/namespaces/${ns}`); - }); }); diff --git a/ui/tests/acceptance/cluster-test.js b/ui/tests/acceptance/cluster-test.js index 26bb8fb121..56dfd1835f 100644 --- a/ui/tests/acceptance/cluster-test.js +++ b/ui/tests/acceptance/cluster-test.js @@ -69,20 +69,4 @@ module('Acceptance | cluster', function (hooks) { await click('[data-test-user-menu-trigger]'); assert.dom('[data-test-user-menu-item="mfa"]').doesNotExist(); }); - - test('enterprise nav item links to first route that user has access to', async function (assert) { - const read_rgp_policy = ` - path "sys/policies/rgp" { - capabilities = ["read"] - }, - `; - - const userToken = await tokenWithPolicy('show-policies-nav', read_rgp_policy); - await logout.visit(); - await authPage.login(userToken); - await visit('/vault/access'); - - assert.dom('[data-test-sidebar-nav-link="Policies"]').hasAttribute('href', '/ui/vault/policies/rgp'); - await logout.visit(); - }); }); diff --git a/ui/tests/acceptance/enterprise-control-groups-test.js b/ui/tests/acceptance/enterprise-control-groups-test.js deleted file mode 100644 index f5307acc51..0000000000 --- a/ui/tests/acceptance/enterprise-control-groups-test.js +++ /dev/null @@ -1,228 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { settled, currentURL, currentRouteName, visit, waitUntil } from '@ember/test-helpers'; -import { module, test, skip } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { create } from 'ember-cli-page-object'; - -import { storageKey } from 'vault/services/control-group'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; -import authForm from 'vault/tests/pages/components/auth-form'; -import controlGroup from 'vault/tests/pages/components/control-group'; -import controlGroupSuccess from 'vault/tests/pages/components/control-group-success'; -import authPage from 'vault/tests/pages/auth'; -import editPage from 'vault/tests/pages/secrets/backend/kv/edit-secret'; -import listPage from 'vault/tests/pages/secrets/backend/list'; - -const consoleComponent = create(consoleClass); -const authFormComponent = create(authForm); -const controlGroupComponent = create(controlGroup); -const controlGroupSuccessComponent = create(controlGroupSuccess); - -module('Acceptance | Enterprise | control groups', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - const POLICY = ` - path "kv/foo" { - capabilities = ["create", "read", "update", "delete", "list"] - control_group = { - max_ttl = "24h" - factor "ops_manager" { - identity { - group_names = ["managers"] - approvals = 1 - } - } - } - } - - path "kv-v2-mount/data/foo" { - capabilities = ["create", "read", "update", "list"] - control_group = { - max_ttl = "24h" - factor "ops_manager" { - identity { - group_names = ["managers"] - approvals = 1 - } - } - } - } - - path "kv-v2-mount/*" { - capabilities = ["list"] - } - `; - - const AUTHORIZER_POLICY = ` - path "sys/control-group/authorize" { - capabilities = ["update"] - } - - path "sys/control-group/request" { - capabilities = ["update"] - } - `; - - const ADMIN_USER = 'authorizer'; - const ADMIN_PASSWORD = 'test'; - const setupControlGroup = async (context) => { - await visit('/vault/secrets'); - await consoleComponent.toggle(); - await settled(); - await consoleComponent.runCommands([ - //enable kv-v1 mount and write a secret - 'write sys/mounts/kv type=kv', - 'write kv/foo bar=baz', - - //enable userpass, create user and associated entity - 'write sys/auth/userpass type=userpass', - `write auth/userpass/users/${ADMIN_USER} password=${ADMIN_PASSWORD} policies=default`, - `write identity/entity name=${ADMIN_USER} policies=test`, - // write policies for control group + authorization - `write sys/policies/acl/kv-control-group policy=${btoa(POLICY)}`, - `write sys/policies/acl/authorizer policy=${btoa(AUTHORIZER_POLICY)}`, - // read out mount to get the accessor - 'read -field=accessor sys/internal/ui/mounts/auth/userpass', - ]); - await settled(); - const userpassAccessor = consoleComponent.lastTextOutput; - - await consoleComponent.runCommands([ - // lookup entity id for our authorizer - `write -field=id identity/lookup/entity name=${ADMIN_USER}`, - ]); - await settled(); - const authorizerEntityId = consoleComponent.lastTextOutput; - await consoleComponent.runCommands([ - // create alias for authorizor and add them to the managers group - `write identity/alias mount_accessor=${userpassAccessor} entity_id=${authorizerEntityId} name=${ADMIN_USER}`, - `write identity/group name=managers member_entity_ids=${authorizerEntityId} policies=authorizer`, - // create a token to request access to kv/foo - 'write -field=client_token auth/token/create policies=kv-control-group', - ]); - await settled(); - context.userToken = consoleComponent.lastLogOutput; - - await authPage.login(context.userToken); - await settled(); - return this; - }; - - const writeSecret = async function (backend, path, key, val) { - await listPage.visitRoot({ backend }); - await listPage.create(); - await editPage.createSecret(path, key, val); - }; - - test('for v2 secrets it redirects you if you try to navigate to a Control Group restricted path', async function (assert) { - await consoleComponent.runCommands([ - 'write sys/mounts/kv-v2-mount type=kv-v2', - 'delete kv-v2-mount/metadata/foo', - ]); - await writeSecret('kv-v2-mount', 'foo', 'bar', 'baz'); - await settled(); - await setupControlGroup(this); - await settled(); - await visit('/vault/secrets/kv-v2-mount/show/foo'); - - assert.ok( - await waitUntil(() => currentRouteName() === 'vault.cluster.access.control-group-accessor'), - 'redirects to access control group route' - ); - }); - - const workflow = async (assert, context, shouldStoreToken) => { - const url = '/vault/secrets/kv/show/foo'; - await setupControlGroup(context); - await settled(); - // as the requestor, go to the URL that's blocked by the control group - // and store the values - await visit(url); - - const accessor = controlGroupComponent.accessor; - const controlGroupToken = controlGroupComponent.token; - await authPage.logout(); - await settled(); - // log in as the admin, navigate to the accessor page, - // and authorize the control group request - await visit('/vault/auth?with=userpass'); - - await authFormComponent.username(ADMIN_USER); - await settled(); - await authFormComponent.password(ADMIN_PASSWORD); - await settled(); - await authFormComponent.login(); - await settled(); - await visit(`/vault/access/control-groups/${accessor}`); - - // putting here to help with flaky test - assert.dom('[data-test-authorize-button]').exists(); - await controlGroupComponent.authorize(); - await settled(); - assert.strictEqual(controlGroupComponent.bannerPrefix, 'Thanks!', 'text display changes'); - await settled(); - await authPage.logout(); - await settled(); - await authPage.login(context.userToken); - await settled(); - if (shouldStoreToken) { - localStorage.setItem( - storageKey(accessor, 'kv/foo'), - JSON.stringify({ - accessor, - token: controlGroupToken, - creation_path: 'kv/foo', - uiParams: { - url, - }, - }) - ); - await visit(`/vault/access/control-groups/${accessor}`); - - assert.ok(controlGroupSuccessComponent.showsNavigateMessage, 'shows user the navigate message'); - await controlGroupSuccessComponent.navigate(); - await settled(); - assert.strictEqual(currentURL(), url, 'successfully loads the target url'); - } else { - await visit(`/vault/access/control-groups/${accessor}`); - - await controlGroupSuccessComponent.token(controlGroupToken); - await settled(); - await controlGroupSuccessComponent.unwrap(); - await settled(); - assert.ok(controlGroupSuccessComponent.showsJsonViewer, 'shows the json viewer'); - } - }; - - skip('it allows the full flow to work without a saved token', async function (assert) { - await workflow(assert, this); - await settled(); - }); - - skip('it allows the full flow to work with a saved token', async function (assert) { - await workflow(assert, this, true); - await settled(); - }); - - test('it displays the warning in the console when making a request to a Control Group path', async function (assert) { - await setupControlGroup(this); - await settled(); - await consoleComponent.toggle(); - await settled(); - await consoleComponent.runCommands('read kv/foo'); - await settled(); - const output = consoleComponent.lastLogOutput; - assert.ok(output.includes('A Control Group was encountered at kv/foo')); - assert.ok(output.includes('The Control Group Token is')); - assert.ok(output.includes('The Accessor is')); - assert.ok(output.includes('Visit /ui/vault/access/control-groups/')); - }); -}); diff --git a/ui/tests/acceptance/enterprise-kmse-test.js b/ui/tests/acceptance/enterprise-kmse-test.js deleted file mode 100644 index 2b61129d83..0000000000 --- a/ui/tests/acceptance/enterprise-kmse-test.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { click, fillIn } from '@ember/test-helpers'; -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; -import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; -import { setupMirage } from 'ember-cli-mirage/test-support'; - -module('Acceptance | Enterprise | keymgmt', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(async function () { - await logout.visit(); - return authPage.login(); - }); - - test('it should add new key and distribute to provider', async function (assert) { - const path = `keymgmt-${Date.now()}`; - this.server.post(`/${path}/key/test-key`, () => ({})); - this.server.put(`/${path}/kms/test-keyvault/key/test-key`, () => ({})); - - await mountSecrets.enable('keymgmt', path); - await click('[data-test-secret-create]'); - await fillIn('[data-test-input="provider"]', 'azurekeyvault'); - await fillIn('[data-test-input="name"]', 'test-keyvault'); - await fillIn('[data-test-input="keyCollection"]', 'test-keycollection'); - await fillIn('[data-test-input="credentials.client_id"]', '123'); - await fillIn('[data-test-input="credentials.client_secret"]', '456'); - await fillIn('[data-test-input="credentials.tenant_id"]', '789'); - await click('[data-test-kms-provider-submit]'); - await click('[data-test-distribute-key]'); - await click('[data-test-component="search-select"] .ember-basic-dropdown-trigger'); - await fillIn('.ember-power-select-search-input', 'test-key'); - await click('.ember-power-select-option'); - await fillIn('[data-test-keymgmt-dist-keytype]', 'rsa-2048'); - await click('[data-test-operation="encrypt"]'); - await fillIn('[data-test-protection="hsm"]', 'hsm'); - - this.server.get(`/${path}/kms/test-keyvault/key`, () => ({ data: { keys: ['test-key'] } })); - await click('[data-test-secret-save]'); - await click('[data-test-kms-provider-tab="keys"] a'); - assert.dom('[data-test-secret-link="test-key"]').exists('Key is listed under keys tab of provider'); - }); -}); diff --git a/ui/tests/acceptance/enterprise-namespaces-test.js b/ui/tests/acceptance/enterprise-namespaces-test.js deleted file mode 100644 index 63429fd73e..0000000000 --- a/ui/tests/acceptance/enterprise-namespaces-test.js +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { click, settled, visit, fillIn, currentURL } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { create } from 'ember-cli-page-object'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; -import authPage from 'vault/tests/pages/auth'; -import logout from 'vault/tests/pages/logout'; - -const shell = create(consoleClass); - -const createNS = async (name) => shell.runCommands(`write sys/namespaces/${name} -force`); - -module('Acceptance | Enterprise | namespaces', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - return authPage.login(); - }); - - test('it clears namespaces when you log out', async function (assert) { - const ns = 'foo'; - await createNS(ns); - await shell.runCommands(`write -field=client_token auth/token/create policies=default`); - const token = shell.lastLogOutput; - await logout.visit(); - await authPage.login(token); - await click('[data-test-namespace-toggle]'); - assert.dom('[data-test-current-namespace]').hasText('root', 'root renders as current namespace'); - assert.dom('[data-test-namespace-link]').doesNotExist('Additional namespace have been cleared'); - await logout.visit(); - }); - - test('it shows nested namespaces if you log in with a namspace starting with a /', async function (assert) { - assert.expect(5); - - await click('[data-test-namespace-toggle]'); - - const nses = ['beep', 'boop', 'bop']; - for (const [i, ns] of nses.entries()) { - await createNS(ns); - await settled(); - // the namespace path will include all of the namespaces up to this point - const targetNamespace = nses.slice(0, i + 1).join('/'); - const url = `/vault/secrets?namespace=${targetNamespace}`; - // this is usually triggered when creating a ns in the form -- trigger a reload of the namespaces manually - await click('[data-test-refresh-namespaces]'); - // check that the single namespace "beep" or "boop" not "beep/boop" shows in the toggle display - assert - .dom(`[data-test-namespace-link="${targetNamespace}"]`) - .hasText(ns, `shows the namespace ${ns} in the toggle component`); - // because quint does not like page reloads, visiting url directing instead of clicking on namespace in toggle - await visit(url); - } - - await logout.visit(); - await settled(); - await authPage.visit({ namespace: '/beep/boop' }); - await settled(); - await authPage.tokenInput('root').submit(); - await settled(); - await click('[data-test-namespace-toggle]'); - - assert.dom('[data-test-current-namespace]').hasText('/beep/boop/', 'current namespace begins with a /'); - assert - .dom('[data-test-namespace-link="beep/boop/bop"]') - .exists('renders the link to the nested namespace'); - }); - - test('it shows the regular namespace toolbar when not managed', async function (assert) { - // This test is the opposite of the test in managed-namespace-test - await logout.visit(); - assert.strictEqual(currentURL(), '/vault/auth?with=token', 'Does not redirect'); - assert.dom('[data-test-namespace-toolbar]').exists('Normal namespace toolbar exists'); - assert - .dom('[data-test-managed-namespace-toolbar]') - .doesNotExist('Managed namespace toolbar does not exist'); - assert.dom('input#namespace').hasAttribute('placeholder', '/ (Root)'); - await fillIn('input#namespace', '/foo'); - const encodedNamespace = encodeURIComponent('/foo'); - assert.strictEqual( - currentURL(), - `/vault/auth?namespace=${encodedNamespace}&with=token`, - 'Does not prepend root to namespace' - ); - }); -}); diff --git a/ui/tests/acceptance/enterprise-oidc-namespace-test.js b/ui/tests/acceptance/enterprise-oidc-namespace-test.js deleted file mode 100644 index a28a38792c..0000000000 --- a/ui/tests/acceptance/enterprise-oidc-namespace-test.js +++ /dev/null @@ -1,91 +0,0 @@ -import { visit, currentURL } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { create } from 'ember-cli-page-object'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import parseURL from 'core/utils/parse-url'; -import consoleClass from 'vault/tests/pages/components/console/ui-panel'; -import authPage from 'vault/tests/pages/auth'; - -const shell = create(consoleClass); - -const createNS = async (name) => { - await shell.runCommands(`write sys/namespaces/${name} -force`); -}; -const SELECTORS = { - authTab: (path) => `[data-test-auth-method="${path}"] a`, -}; - -module('Acceptance | Enterprise | oidc auth namespace test', function (hooks) { - setupApplicationTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(async function () { - this.namespace = 'test-ns'; - this.rootOidc = 'root-oidc'; - this.nsOidc = 'ns-oidc'; - - this.server.post(`/auth/:path/config`, () => {}); - - this.enableOidc = (path, role = '') => { - return shell.runCommands([ - `write sys/auth/${path} type=oidc`, - `write auth/${path}/config default_role="${role}" oidc_discovery_url="https://example.com"`, - // show method as tab - `write sys/auth/${path}/tune listing_visibility="unauth"`, - ]); - }; - - this.disableOidc = (path) => shell.runCommands([`delete /sys/auth/${path}`]); - }); - - test('oidc: request is made to auth_url when a namespace is inputted', async function (assert) { - assert.expect(5); - - this.server.post(`/auth/${this.rootOidc}/oidc/auth_url`, (schema, req) => { - const { redirect_uri } = JSON.parse(req.requestBody); - const { pathname, search } = parseURL(redirect_uri); - assert.strictEqual( - pathname + search, - `/ui/vault/auth/${this.rootOidc}/oidc/callback`, - 'request made to auth_url when the login page is visited' - ); - }); - this.server.post(`/auth/${this.nsOidc}/oidc/auth_url`, (schema, req) => { - const { redirect_uri } = JSON.parse(req.requestBody); - const { pathname, search } = parseURL(redirect_uri); - assert.strictEqual( - pathname + search, - `/ui/vault/auth/${this.nsOidc}/oidc/callback?namespace=${this.namespace}`, - 'request made to correct auth_url when namespace is filled in' - ); - }); - - await authPage.login(); - // enable oidc in root namespace, without default role - await this.enableOidc(this.rootOidc); - // create child namespace to enable oidc - await createNS(this.namespace); - // enable oidc in child namespace with default role - await authPage.loginNs(this.namespace); - await this.enableOidc(this.nsOidc, `${this.nsOidc}-role`); - await authPage.logout(); - - await visit('/vault/auth'); - assert.dom(SELECTORS.authTab(this.rootOidc)).exists('renders oidc method tab for root'); - await authPage.namespaceInput(this.namespace); - assert.strictEqual( - currentURL(), - `/vault/auth?namespace=${this.namespace}&with=${this.nsOidc}%2F`, - 'url updates with namespace value' - ); - assert.dom(SELECTORS.authTab(this.nsOidc)).exists('renders oidc method tab for child namespace'); - - // disable methods to cleanup test state for re-running - await authPage.login(); - await this.disableOidc(this.rootOidc); - await this.disableOidc(this.nsOidc); - await shell.runCommands([`delete /sys/auth/${this.namespace}`]); - await authPage.logout(); - }); -}); diff --git a/ui/tests/acceptance/managed-namespace-test.js b/ui/tests/acceptance/managed-namespace-test.js deleted file mode 100644 index 6f6bb4061e..0000000000 --- a/ui/tests/acceptance/managed-namespace-test.js +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { module, test } from 'qunit'; -import { currentURL, visit, fillIn } from '@ember/test-helpers'; -import { setupApplicationTest } from 'ember-qunit'; -import Pretender from 'pretender'; -import logout from 'vault/tests/pages/logout'; -import { getManagedNamespace } from 'vault/routes/vault/cluster'; - -const FEATURE_FLAGS_RESPONSE = { - feature_flags: ['VAULT_CLOUD_ADMIN_NAMESPACE'], -}; - -module('Acceptance | Enterprise | Managed namespace root', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - /** - * Since the features are fetched on the application load, - * we have to populate them on the beforeEach hook because - * the fetch won't trigger again within the tests - */ - this.server = new Pretender(function () { - this.get('/v1/sys/internal/ui/feature-flags', () => { - return [200, { 'Content-Type': 'application/json' }, JSON.stringify(FEATURE_FLAGS_RESPONSE)]; - }); - this.get('/v1/sys/health', this.passthrough); - this.get('/v1/sys/seal-status', this.passthrough); - this.get('/v1/sys/license/features', this.passthrough); - this.get('/v1/sys/internal/ui/mounts', this.passthrough); - }); - }); - - hooks.afterEach(function () { - this.server.shutdown(); - }); - - test('it shows the managed namespace toolbar when feature flag exists', async function (assert) { - await logout.visit(); - await visit('/vault/auth'); - assert.ok(currentURL().startsWith('/vault/auth'), 'Redirected to auth'); - assert.ok(currentURL().includes('?namespace=admin'), 'with base namespace'); - assert.dom('[data-test-namespace-toolbar]').doesNotExist('Normal namespace toolbar does not exist'); - assert.dom('[data-test-managed-namespace-toolbar]').exists('Managed namespace toolbar exists'); - assert.dom('[data-test-managed-namespace-root]').hasText('/admin', 'Shows /admin namespace prefix'); - assert.dom('input#namespace').hasAttribute('placeholder', '/ (Default)'); - await fillIn('input#namespace', '/foo'); - const encodedNamespace = encodeURIComponent('admin/foo'); - assert.strictEqual( - currentURL(), - `/vault/auth?namespace=${encodedNamespace}&with=token`, - 'Correctly prepends root to namespace' - ); - }); - - test('getManagedNamespace helper works as expected', function (assert) { - let managedNs = getManagedNamespace(null, 'admin'); - assert.strictEqual(managedNs, 'admin', 'returns root ns when no namespace present'); - managedNs = getManagedNamespace('admin/', 'admin'); - assert.strictEqual(managedNs, 'admin', 'returns root ns when matches passed ns'); - managedNs = getManagedNamespace('adminfoo/', 'admin'); - assert.strictEqual( - managedNs, - 'admin/adminfoo/', - 'appends passed namespace to root even if it matches without slashes' - ); - managedNs = getManagedNamespace('admin/foo/', 'admin'); - assert.strictEqual(managedNs, 'admin/foo/', 'returns passed namespace if it starts with root and /'); - }); - - test('it redirects to root prefixed ns when non-root passed', async function (assert) { - await logout.visit(); - await visit('/vault/auth?namespace=admindev'); - assert.ok(currentURL().startsWith('/vault/auth'), 'Redirected to auth'); - assert.ok( - currentURL().includes(`?namespace=${encodeURIComponent('admin/admindev')}`), - 'with appended namespace' - ); - - assert.dom('[data-test-managed-namespace-root]').hasText('/admin', 'Shows /admin namespace prefix'); - assert.dom('input#namespace').hasValue('/admindev', 'Input has /dev value'); - }); -}); diff --git a/ui/tests/acceptance/oidc-config/clients-assignments-test.js b/ui/tests/acceptance/oidc-config/clients-assignments-test.js index b774dadd6f..628678071c 100644 --- a/ui/tests/acceptance/oidc-config/clients-assignments-test.js +++ b/ui/tests/acceptance/oidc-config/clients-assignments-test.js @@ -69,8 +69,8 @@ module('Acceptance | oidc-config clients and assignments', function (hooks) { assert.strictEqual(currentURL(), '/vault/access/oidc'); assert.dom('h1.title.is-3').hasText('OIDC Provider'); assert.dom(SELECTORS.oidcHeader).hasText( - `Configure Vault to act as an OIDC identity provider, and offer Vault’s various authentication - methods and source of identity to any client applications. Learn more Create your first app`, + `Configure OpenBao to act as an OIDC identity provider, and offer OpenBao’s various authentication + methods and source of identity to any client applications. Create your first app`, 'renders call to action header when no clients are configured' ); assert.dom('[data-test-oidc-landing]').exists('landing page renders when no clients are configured'); diff --git a/ui/tests/acceptance/pki/pki-action-forms-test.js b/ui/tests/acceptance/pki/pki-action-forms-test.js index e4b2c892bb..f7f02866c8 100644 --- a/ui/tests/acceptance/pki/pki-action-forms-test.js +++ b/ui/tests/acceptance/pki/pki-action-forms-test.js @@ -184,7 +184,7 @@ module('Acceptance | pki action forms test', function (hooks) { assert.dom(S.configuration.emptyState).doesNotExist(); // The URLs section is populated based on params returned from OpenAPI. This test will break when // the backend adds fields. We should update the count accordingly. - assert.dom(S.configuration.urlField).exists({ count: 4 }); + assert.dom(S.configuration.urlField).exists({ count: 5 }); // Fill in form await fillIn(S.configuration.typeField, 'internal'); await typeIn(S.configuration.inputByName('commonName'), commonName); diff --git a/ui/tests/acceptance/secrets/backend/alicloud/secret-test.js b/ui/tests/acceptance/secrets/backend/alicloud/secret-test.js deleted file mode 100644 index 6e7306a618..0000000000 --- a/ui/tests/acceptance/secrets/backend/alicloud/secret-test.js +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { currentRouteName, settled } from '@ember/test-helpers'; -import { module, test } from 'qunit'; -import { setupApplicationTest } from 'ember-qunit'; -import { v4 as uuidv4 } from 'uuid'; - -import mountSecrets from 'vault/tests/pages/settings/mount-secret-backend'; -import backendsPage from 'vault/tests/pages/secrets/backends'; -import authPage from 'vault/tests/pages/auth'; - -module('Acceptance | alicloud/enable', function (hooks) { - setupApplicationTest(hooks); - - hooks.beforeEach(function () { - this.uid = uuidv4(); - return authPage.login(); - }); - - test('enable alicloud', async function (assert) { - const enginePath = `alicloud-${this.uid}`; - await mountSecrets.visit(); - await settled(); - await mountSecrets.selectType('alicloud'); - await settled(); - await mountSecrets.next().path(enginePath).submit(); - await settled(); - - assert.strictEqual( - currentRouteName(), - 'vault.cluster.secrets.backends', - 'redirects to the backends page' - ); - await settled(); - assert.ok(backendsPage.rows.filterBy('path', `${enginePath}/`)[0], 'shows the alicloud engine'); - }); -}); diff --git a/ui/tests/acceptance/secrets/backend/database/secret-test.js b/ui/tests/acceptance/secrets/backend/database/secret-test.js index 2360416f95..6c9801a107 100644 --- a/ui/tests/acceptance/secrets/backend/database/secret-test.js +++ b/ui/tests/acceptance/secrets/backend/database/secret-test.js @@ -34,12 +34,14 @@ const mount = async () => { return path; }; -const newConnection = async (backend, plugin = 'mongodb-database-plugin') => { +const newConnection = async (backend, plugin = 'mysql-database-plugin') => { const name = `connection-${Date.now()}`; await connectionPage.visitCreate({ backend }); await connectionPage.dbPlugin(plugin); await connectionPage.name(name); - await connectionPage.connectionUrl(`mongodb://127.0.0.1:4321/${name}`); + await connectionPage.connectionUrl(`{{username}}:{{password}}@tcp(127.0.0.1:3306)/${name}`); + await connectionPage.username('user'); + await connectionPage.password('so-secure'); await connectionPage.toggleVerify(); await connectionPage.save(); await connectionPage.enable(); @@ -192,7 +194,7 @@ module('Acceptance | secrets/database/*', function (hooks) { }); test('Connection create and edit form for each plugin', async function (assert) { - assert.expect(161); + assert.expect(95); const backend = await mount(); for (const testCase of connectionTests) { await connectionPage.visitCreate({ backend }); @@ -203,13 +205,7 @@ module('Acceptance | secrets/database/*', function (hooks) { await connectionPage.dbPlugin(testCase.plugin); assert.dom('[data-test-empty-state]').doesNotExist('Empty state goes away after plugin selected'); await connectionPage.name(testCase.name); - if (testCase.plugin === 'elasticsearch-database-plugin') { - await connectionPage.url(testCase.url); - await connectionPage.username(testCase.elasticUser); - await connectionPage.password(testCase.elasticPassword); - } else { - await connectionPage.connectionUrl(testCase.url); - } + await connectionPage.connectionUrl(testCase.url); testCase.requiredFields(assert, testCase.name); await connectionPage.toggleVerify(); await connectionPage.save(); @@ -251,14 +247,17 @@ module('Acceptance | secrets/database/*', function (hooks) { test('Can create and delete a connection', async function (assert) { const backend = await mount(); const connectionDetails = { - plugin: 'mongodb-database-plugin', + plugin: 'mysql-database-plugin', id: 'horses-db', fields: [ { label: 'Connection name', name: 'name', value: 'horses-db' }, - { label: 'Connection URL', name: 'connection_url', value: 'mongodb://127.0.0.1:235/horses' }, + { + label: 'Connection URL', + name: 'connection_url', + value: '{{username}}:{{password}}@tcp(127.0.0.1:3306)/', + }, { label: 'Username', name: 'username', value: 'user', hideOnShow: true }, { label: 'Password', name: 'password', password: 'so-secure', hideOnShow: true }, - { label: 'Write concern', name: 'write_concern' }, ], }; assert.strictEqual( diff --git a/ui/tests/acceptance/secrets/backend/engines-test.js b/ui/tests/acceptance/secrets/backend/engines-test.js index 60fd9a2a8f..734d48dad5 100644 --- a/ui/tests/acceptance/secrets/backend/engines-test.js +++ b/ui/tests/acceptance/secrets/backend/engines-test.js @@ -29,14 +29,13 @@ module('Acceptance | secret-engine list view', function (hooks) { test('it allows you to disable an engine', async function (assert) { // first mount an engine so we can disable it. - const enginePath = `alicloud-disable-${this.uid}`; - await mountSecrets.enable('alicloud', enginePath); + const enginePath = `pki-disable-${this.uid}`; + await mountSecrets.enable('pki', enginePath); await settled(); - assert.ok(backendsPage.rows.filterBy('path', `${enginePath}/`)[0], 'shows the mounted engine'); - await backendsPage.visit(); await settled(); const row = backendsPage.rows.filterBy('path', `${enginePath}/`)[0]; + assert.ok(row, 'shows the mounted engine'); await row.menu(); await settled(); await backendsPage.disableButton(); @@ -55,48 +54,23 @@ module('Acceptance | secret-engine list view', function (hooks) { ); }); - test('it adds disabled css styling to unsupported secret engines', async function (assert) { - assert.expect(2); - // first mount engine that is not supported - const enginePath = `nomad-${this.uid}`; - - await mountSecrets.enable('nomad', enginePath); - await settled(); - await backendsPage.visit(); - await settled(); - - const rows = document.querySelectorAll('[data-test-auth-backend-link]'); - const rowUnsupported = Array.from(rows).filter((row) => row.innerText.includes('nomad')); - const rowSupported = Array.from(rows).filter((row) => row.innerText.includes('cubbyhole')); - assert - .dom(rowUnsupported[0]) - .doesNotHaveClass( - 'linked-block', - `the linked-block class is not added to unsupported engines, which effectively disables it.` - ); - assert.dom(rowSupported[0]).hasClass('linked-block', `linked-block class is added to supported engines.`); - - // cleanup - await consoleComponent.runCommands([`delete sys/mounts/${enginePath}`]); - }); - test('it filters by name and engine type', async function (assert) { assert.expect(4); - const enginePath1 = `aws-1-${this.uid}`; - const enginePath2 = `aws-2-${this.uid}`; + const enginePath1 = `database-1-${this.uid}`; + const enginePath2 = `database-2-${this.uid}`; - await mountSecrets.enable('aws', enginePath1); - await mountSecrets.enable('aws', enginePath2); + await mountSecrets.enable('database', enginePath1); + await mountSecrets.enable('database', enginePath2); await backendsPage.visit(); await settled(); // filter by type await clickTrigger('#filter-by-engine-type'); - await searchSelect.options.objectAt(0).click(); + await searchSelect.options.objectAt(1).click(); const rows = document.querySelectorAll('[data-test-auth-backend-link]'); - const rowsAws = Array.from(rows).filter((row) => row.innerText.includes('aws')); + const rowsAws = Array.from(rows).filter((row) => row.innerText.includes('database')); - assert.strictEqual(rows.length, rowsAws.length, 'all rows returned are aws'); + assert.strictEqual(rows.length, rowsAws.length, 'all rows returned are database'); // filter by name await clickTrigger('#filter-by-engine-name'); const firstItemToSelect = searchSelect.options.objectAt(0).text; diff --git a/ui/tests/acceptance/settings-test.js b/ui/tests/acceptance/settings-test.js index f4210aa165..1d9147d595 100644 --- a/ui/tests/acceptance/settings-test.js +++ b/ui/tests/acceptance/settings-test.js @@ -26,7 +26,7 @@ module('Acceptance | settings', function (hooks) { }); test('settings', async function (assert) { - const type = 'consul'; + const type = 'pki'; const path = `settings-path-${this.uid}`; // mount unsupported backend @@ -49,7 +49,13 @@ module('Acceptance | settings', function (hooks) { `Successfully mounted '${type}' at '${path}'!` ); await settled(); - assert.strictEqual(currentURL(), `/vault/secrets`, 'redirects to secrets page'); + assert.strictEqual( + currentURL(), + `/vault/secrets/${path}/pki/overview`, + 'redirects to secrets settings page' + ); + await backendListPage.visit(); + await settled(); const row = backendListPage.rows.filterBy('path', path + '/')[0]; await row.menu(); await backendListPage.configLink(); diff --git a/ui/tests/acceptance/settings/auth/configure/index-test.js b/ui/tests/acceptance/settings/auth/configure/index-test.js index ce04431098..8db2d0df4b 100644 --- a/ui/tests/acceptance/settings/auth/configure/index-test.js +++ b/ui/tests/acceptance/settings/auth/configure/index-test.js @@ -34,14 +34,14 @@ module('Acceptance | settings/auth/configure', function (hooks) { }); test('it redirects to the first section', async function (assert) { - const path = `aws-redirect-${this.uid}`; - const type = 'aws'; + const path = `ldap-redirect-${this.uid}`; + const type = 'ldap'; await enablePage.enable(type, path); await page.visit({ path }); assert.strictEqual(currentRouteName(), 'vault.cluster.settings.auth.configure.section'); assert.strictEqual( currentURL(), - `/vault/settings/auth/configure/${path}/client`, + `/vault/settings/auth/configure/${path}/configuration`, 'loads the first section for the type of auth method' ); }); diff --git a/ui/tests/acceptance/settings/auth/configure/section-test.js b/ui/tests/acceptance/settings/auth/configure/section-test.js index 24617ff6b8..295e1c69bf 100644 --- a/ui/tests/acceptance/settings/auth/configure/section-test.js +++ b/ui/tests/acceptance/settings/auth/configure/section-test.js @@ -60,14 +60,14 @@ module('Acceptance | settings/auth/configure/section', function (hooks) { assert.ok(keys.includes('description'), 'passes updated description on tune'); }); - for (const type of ['aws', 'azure', 'gcp', 'github', 'kubernetes']) { + for (const type of ['ldap', 'kubernetes']) { test(`it shows tabs for auth method: ${type}`, async function (assert) { const path = `${type}-showtab-${this.uid}`; await cli.consoleInput(`write sys/auth/${path} type=${type}`); await cli.enter(); await indexPage.visit({ path }); - // aws has 4 tabs, the others will have 'Configuration' and 'Method Options' tabs - const numTabs = type === 'aws' ? 4 : 2; + // items will have 'Configuration' and 'Method Options' tabs + const numTabs = 2; assert.strictEqual(page.tabs.length, numTabs, 'shows correct number of tabs'); }); } diff --git a/ui/tests/acceptance/sidebar-nav-test.js b/ui/tests/acceptance/sidebar-nav-test.js index f68c99563f..c2f0a24502 100644 --- a/ui/tests/acceptance/sidebar-nav-test.js +++ b/ui/tests/acceptance/sidebar-nav-test.js @@ -48,7 +48,7 @@ module('Acceptance | sidebar navigation', function (hooks) { const links = [ { label: 'Raft Storage', route: '/vault/storage/raft' }, - { label: 'Seal Vault', route: '/vault/settings/seal' }, + { label: 'Seal OpenBao', route: '/vault/settings/seal' }, { label: 'Secrets engines', route: '/vault/secrets' }, ]; diff --git a/ui/tests/acceptance/ssh-test.js b/ui/tests/acceptance/ssh-test.js index 461349fd53..6883eb1b9d 100644 --- a/ui/tests/acceptance/ssh-test.js +++ b/ui/tests/acceptance/ssh-test.js @@ -27,6 +27,9 @@ module('Acceptance | ssh secret backend', function (hooks) { name: 'carole', async fillInCreate() { await click('[data-test-input="allowUserCertificates"]'); + await click('[data-test-toggle-group="Options"]'); + await fillIn('[data-test-input="defaultUser"]', 'carol'); + await fillIn('[data-test-input="allowedUsers"]', '*'); }, async fillInGenerate() { await fillIn('[data-test-input="publicKey"]', PUB_KEY); diff --git a/ui/tests/integration/components/mount-backend-form-test.js b/ui/tests/integration/components/mount-backend-form-test.js index f26c584bfc..b40699e55a 100644 --- a/ui/tests/integration/components/mount-backend-form-test.js +++ b/ui/tests/integration/components/mount-backend-form-test.js @@ -59,9 +59,9 @@ module('Integration | Component | mount backend form', function (hooks) { await render( hbs`` ); - await component.selectType('aws'); + await component.selectType('ldap'); await component.next(); - assert.strictEqual(component.pathValue, 'aws', 'sets the value of the type'); + assert.strictEqual(component.pathValue, 'ldap', 'sets the value of the type'); await component.back(); await component.selectType('approle'); await component.next(); @@ -81,9 +81,9 @@ module('Integration | Component | mount backend form', function (hooks) { await component.back(); assert.strictEqual(this.model.type, '', 'Clears type on back'); assert.strictEqual(this.model.path, 'newpath', 'Path is still newPath'); - await component.selectType('aws'); + await component.selectType('ldap'); await component.next(); - assert.strictEqual(this.model.type, 'aws', 'Updates type on model'); + assert.strictEqual(this.model.type, 'ldap', 'Updates type on model'); assert.strictEqual(component.pathValue, 'newpath', 'keeps custom path value'); }); @@ -91,7 +91,7 @@ module('Integration | Component | mount backend form', function (hooks) { await render( hbs`` ); - await component.selectType('github'); + await component.selectType('ldap'); await component.next(); await component.toggleOptions(); assert diff --git a/ui/tests/integration/components/mount-backend/type-form-test.js b/ui/tests/integration/components/mount-backend/type-form-test.js index ffccc7e6b5..8f0a176429 100644 --- a/ui/tests/integration/components/mount-backend/type-form-test.js +++ b/ui/tests/integration/components/mount-backend/type-form-test.js @@ -12,7 +12,7 @@ import { allEngines, mountableEngines } from 'vault/helpers/mountable-secret-eng import { methods } from 'vault/helpers/mountable-auth-methods'; const secretTypes = mountableEngines().map((engine) => engine.type); -const allSecretTypes = allEngines().map((engine) => engine.type); +allEngines().map((engine) => engine.type); const authTypes = methods().map((auth) => auth.type); module('Integration | Component | mount-backend/type-form', function (hooks) { @@ -30,8 +30,8 @@ module('Integration | Component | mount-backend/type-form', function (hooks) { assert .dom('[data-test-mount-type]') .exists({ count: secretTypes.length }, 'Renders all mountable engines'); - await click(`[data-test-mount-type="nomad"]`); - assert.dom(`[data-test-mount-type="nomad"] input`).isChecked(`ssh is checked`); + await click(`[data-test-mount-type="pki"]`); + assert.dom(`[data-test-mount-type="pki"] input`).isChecked(`pki is checked`); assert.ok(spy.notCalled, 'callback not called'); await click(`[data-test-mount-type="ssh"]`); assert.dom(`[data-test-mount-type="ssh"] input`).isChecked(`ssh is checked`); @@ -48,27 +48,13 @@ module('Integration | Component | mount-backend/type-form', function (hooks) { assert .dom('[data-test-mount-type]') .exists({ count: authTypes.length }, 'Renders all mountable auth methods'); - await click(`[data-test-mount-type="okta"]`); - assert.dom(`[data-test-mount-type="okta"] input`).isChecked(`ssh is checked`); + await click(`[data-test-mount-type="ldap"]`); + assert.dom(`[data-test-mount-type="ldap"] input`).isChecked(`ldap is checked`); assert.ok(spy.notCalled, 'callback not called'); - await click(`[data-test-mount-type="github"]`); - assert.dom(`[data-test-mount-type="github"] input`).isChecked(`ssh is checked`); + await click(`[data-test-mount-type="kubernetes"]`); + assert.dom(`[data-test-mount-type="kubernetes"] input`).isChecked(`kubernetes is checked`); assert.ok(spy.notCalled, 'callback not called'); await click('[data-test-mount-next]'); - assert.ok(spy.calledOnceWith('github')); - }); - - module('Enterprise', function (hooks) { - hooks.beforeEach(function () { - this.version = this.owner.lookup('service:version'); - this.version.version = '1.12.1+ent'; - }); - - test('it renders correct items for enterprise secrets', async function (assert) { - await render(hbs``); - assert - .dom('[data-test-mount-type]') - .exists({ count: allSecretTypes.length }, 'Renders all secret engines'); - }); + // assert.ok(spy.calledOnceWith('jwt')); }); }); diff --git a/ui/tests/integration/components/path-filter-config-list-test.js b/ui/tests/integration/components/path-filter-config-list-test.js deleted file mode 100644 index f3384ecd7b..0000000000 --- a/ui/tests/integration/components/path-filter-config-list-test.js +++ /dev/null @@ -1,171 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, click, findAll } from '@ember/test-helpers'; -import { typeInSearch, clickTrigger } from 'ember-power-select/test-support/helpers'; -import hbs from 'htmlbars-inline-precompile'; -import { setupEngine } from 'ember-engines/test-support'; -import Service from '@ember/service'; -import sinon from 'sinon'; -import { Promise } from 'rsvp'; -import { create } from 'ember-cli-page-object'; -import ss from 'vault/tests/pages/components/search-select'; - -const searchSelect = create(ss); - -const MOUNTS_RESPONSE = { - data: { - secret: {}, - auth: { - 'userpass/': { type: 'userpass', accessor: 'userpass' }, - }, - }, -}; -const NAMESPACE_MOUNTS_RESPONSE = { - data: { - secret: { - 'namespace-kv/': { type: 'kv', accessor: 'kv' }, - }, - auth: {}, - }, -}; - -module('Integration | Component | path filter config list', function (hooks) { - setupRenderingTest(hooks); - setupEngine(hooks, 'replication'); - - hooks.beforeEach(function () { - this.context = { owner: this.engine }; // this.engine set by setupEngine - const ajaxStub = sinon.stub().usingPromise(Promise); - ajaxStub.withArgs('/v1/sys/internal/ui/mounts', 'GET').resolves(MOUNTS_RESPONSE); - ajaxStub - .withArgs('/v1/sys/internal/ui/mounts', 'GET', { namespace: 'ns1' }) - .resolves(NAMESPACE_MOUNTS_RESPONSE); - this.set('ajaxStub', ajaxStub); - const namespaceServiceStub = Service.extend({ - init() { - this._super(...arguments); - this.set('accessibleNamespaces', ['ns1']); - }, - }); - - const storeServiceStub = Service.extend({ - adapterFor() { - return { - ajax: ajaxStub, - }; - }, - }); - this.engine.register('service:namespace', namespaceServiceStub); - this.engine.register('service:store', storeServiceStub); - }); - - test('it renders', async function (assert) { - this.set('config', { mode: null, paths: [] }); - await render(hbs``, this.context); - - assert.dom('[data-test-component=path-filter-config]').exists(); - }); - - test('it sets config.paths', async function (assert) { - this.set('config', { mode: 'allow', paths: [] }); - this.set('paths', []); - await render(hbs``, this.context); - - await clickTrigger(); - await typeInSearch('auth'); - await searchSelect.options.objectAt(1).click(); - assert.ok(this.config.paths.includes('auth/userpass/'), 'adds to paths'); - - await clickTrigger(); - await assert.strictEqual(searchSelect.options.length, 1, 'has one option left'); - - await searchSelect.deleteButtons.objectAt(0).click(); - assert.strictEqual(this.config.paths.length, 0, 'removes from paths'); - await clickTrigger(); - await assert.strictEqual(searchSelect.options.length, 2, 'has both options'); - }); - - test('it sets config.mode', async function (assert) { - this.set('config', { mode: 'allow', paths: [] }); - await render(hbs``, this.context); - await click('#deny'); - assert.strictEqual(this.config.mode, 'deny'); - await click('#no-filtering'); - assert.strictEqual(this.config.mode, null); - }); - - test('it shows a warning when going from a mode to allow all', async function (assert) { - this.set('config', { mode: 'allow', paths: [] }); - await render(hbs``, this.context); - await click('#no-filtering'); - assert.dom('[data-test-remove-warning]').exists('shows removal warning'); - }); - - test('it fetches mounts from a namespace when namespace name is entered', async function (assert) { - this.set('config', { mode: 'allow', paths: [] }); - this.set('paths', []); - await render(hbs``, this.context); - - await clickTrigger(); - assert.strictEqual(searchSelect.options.length, 2, 'shows userpass and namespace as an option'); - // type the namespace to trigger an ajax request - await typeInSearch('ns1'); - assert.strictEqual(searchSelect.options.length, 2, 'has ns and ns mount in the list'); - await searchSelect.options.objectAt(1).click(); - assert.ok(this.config.paths.includes('ns1/namespace-kv/'), 'adds namespace mount to paths'); - }); - - test('it selects mounts from different groups, and puts discarded option back within group', async function (assert) { - this.set('config', { mode: 'allow', paths: [] }); - this.set('paths', []); - await render(hbs``, this.context); - await clickTrigger(); - await searchSelect.options.objectAt(1).click(); - await clickTrigger(); - await typeInSearch('ns1'); - await searchSelect.options.objectAt(1).click(); - await clickTrigger(); - await searchSelect.options.objectAt(0).click(); - assert.dom('[data-test-selected-option="0"]').hasText('auth/userpass/', 'renders first mount selected'); - assert - .dom('[data-test-selected-option="1"]') - .hasText('ns1/namespace-kv/', 'renders second mount selected'); - assert.dom('[data-test-selected-option="2"]').hasText('ns1', 'renders third mount selected'); - assert.propEqual( - this.config.paths, - ['auth/userpass/', 'ns1/namespace-kv/', 'ns1'], - 'adds all selections to paths' - ); - await searchSelect.deleteButtons.objectAt(0).click(); - await clickTrigger(); - assert - .dom('.ember-power-select-group') - .hasText('Auth Methods auth/userpass/', 'puts auth method back within group'); - await clickTrigger(); - await searchSelect.deleteButtons.objectAt(1).click(); - await clickTrigger(); - assert.dom('.ember-power-select-group').hasText('Namespaces ns1', 'puts ns back within group'); - await clickTrigger(); - }); - - test('it renders previously set config.paths when editing the mount config', async function (assert) { - this.set('config', { mode: 'allow', paths: ['auth/userpass/'] }); - this.set('paths', []); - await render(hbs``, this.context); - assert.strictEqual( - searchSelect.selectedOptions.objectAt(0).text, - 'auth/userpass/', - 'renders config.path as selected on init' - ); - await clickTrigger(); - assert.strictEqual(findAll('.ember-power-select-group').length, 1, 'renders only remaining group'); - await searchSelect.deleteButtons.objectAt(0).click(); - await clickTrigger(); - assert.strictEqual(findAll('.ember-power-select-group').length, 2, 'renders two groups'); - }); -}); diff --git a/ui/tests/integration/components/pki/page/pki-configuration-details-test.js b/ui/tests/integration/components/pki/page/pki-configuration-details-test.js index 71877f5ca1..a289f8bb8e 100644 --- a/ui/tests/integration/components/pki/page/pki-configuration-details-test.js +++ b/ui/tests/integration/components/pki/page/pki-configuration-details-test.js @@ -163,33 +163,6 @@ module('Integration | Component | Page::PkiConfigurationDetails', function (hook assert.dom(SELECTORS.rowValue('Delta rebuild interval')).doesNotExist(); }); - test('it renders enterprise params in crl section', async function (assert) { - this.version = this.owner.lookup('service:version'); - this.version.version = '1.13.1+ent'; - await render( - hbs`,`, - { owner: this.engine } - ); - assert.dom(SELECTORS.rowValue('Cross-cluster revocation')).hasText('Yes'); - assert.dom(SELECTORS.rowIcon('Cross-cluster revocation', 'check-circle')); - assert.dom(SELECTORS.rowValue('Unified CRL')).hasText('Yes'); - assert.dom(SELECTORS.rowIcon('Unified CRL', 'check-circle')); - assert.dom(SELECTORS.rowValue('Unified CRL on existing paths')).hasText('Yes'); - assert.dom(SELECTORS.rowIcon('Unified CRL on existing paths', 'check-circle')); - }); - - test('it does not render enterprise params in crl section', async function (assert) { - this.version = this.owner.lookup('service:version'); - this.version.version = '1.13.1'; - await render( - hbs`,`, - { owner: this.engine } - ); - assert.dom(SELECTORS.rowValue('Cross-cluster revocation')).doesNotExist(); - assert.dom(SELECTORS.rowValue('Unified CRL')).doesNotExist(); - assert.dom(SELECTORS.rowValue('Unified CRL on existing paths')).doesNotExist(); - }); - test('shows the correct information on mount configuration section', async function (assert) { await render( hbs`,`, diff --git a/ui/tests/integration/components/pki/page/pki-configuration-edit-test.js b/ui/tests/integration/components/pki/page/pki-configuration-edit-test.js index 13e733fb2a..0cf85bbce2 100644 --- a/ui/tests/integration/components/pki/page/pki-configuration-edit-test.js +++ b/ui/tests/integration/components/pki/page/pki-configuration-edit-test.js @@ -273,105 +273,6 @@ module('Integration | Component | page/pki-configuration-edit', function (hooks) await click(SELECTORS.saveButton); }); - test('it renders enterprise only params', async function (assert) { - assert.expect(6); - this.version = this.owner.lookup('service:version'); - this.version.version = '1.13.1+ent'; - this.server.post(`/${this.backend}/config/acme`, () => {}); - this.server.post(`/${this.backend}/config/cluster`, () => {}); - this.server.post(`/${this.backend}/config/crl`, (schema, req) => { - assert.ok(true, 'request made to save crl config'); - assert.propEqual( - JSON.parse(req.requestBody), - { - auto_rebuild: false, - auto_rebuild_grace_period: '12h', - delta_rebuild_interval: '15m', - disable: false, - enable_delta: false, - expiry: '72h', - ocsp_disable: false, - ocsp_expiry: '12h', - cross_cluster_revocation: true, - unified_crl: true, - unified_crl_on_existing_paths: true, - }, - 'crl payload includes enterprise params' - ); - }); - this.server.post(`/${this.backend}/config/urls`, () => { - assert.ok(true, 'request made to save urls config'); - }); - await render( - hbs` - - `, - this.context - ); - - assert.dom(SELECTORS.groupHeader('Certificate Revocation List (CRL)')).exists(); - assert.dom(SELECTORS.groupHeader('Online Certificate Status Protocol (OCSP)')).exists(); - assert.dom(SELECTORS.groupHeader('Unified Revocation')).exists(); - await click(SELECTORS.checkboxInput('crossClusterRevocation')); - await click(SELECTORS.checkboxInput('unifiedCrl')); - await click(SELECTORS.checkboxInput('unifiedCrlOnExistingPaths')); - await click(SELECTORS.saveButton); - }); - - test('it does not render enterprise only params for OSS', async function (assert) { - assert.expect(9); - this.version = this.owner.lookup('service:version'); - this.version.version = '1.13.1'; - this.server.post(`/${this.backend}/config/acme`, () => {}); - this.server.post(`/${this.backend}/config/cluster`, () => {}); - this.server.post(`/${this.backend}/config/crl`, (schema, req) => { - assert.ok(true, 'request made to save crl config'); - assert.propEqual( - JSON.parse(req.requestBody), - { - auto_rebuild: false, - auto_rebuild_grace_period: '12h', - delta_rebuild_interval: '15m', - disable: false, - enable_delta: false, - expiry: '72h', - ocsp_disable: false, - ocsp_expiry: '12h', - }, - 'crl payload does not include enterprise params' - ); - }); - this.server.post(`/${this.backend}/config/urls`, () => { - assert.ok(true, 'request made to save urls config'); - }); - await render( - hbs` - - `, - this.context - ); - - assert.dom(SELECTORS.checkboxInput('crossClusterRevocation')).doesNotExist(); - assert.dom(SELECTORS.checkboxInput('unifiedCrl')).doesNotExist(); - assert.dom(SELECTORS.checkboxInput('unifiedCrlOnExistingPaths')).doesNotExist(); - assert.dom(SELECTORS.groupHeader('Certificate Revocation List (CRL)')).exists(); - assert.dom(SELECTORS.groupHeader('Online Certificate Status Protocol (OCSP)')).exists(); - assert.dom(SELECTORS.groupHeader('Unified Revocation')).doesNotExist(); - await click(SELECTORS.saveButton); - }); - test('it renders empty states if no update capabilities', async function (assert) { assert.expect(4); this.server.post('/sys/capabilities-self', allowAllCapabilitiesStub(['read'])); diff --git a/ui/tests/integration/components/pki/pki-generate-root-test.js b/ui/tests/integration/components/pki/pki-generate-root-test.js index 8cd4925c6b..8fb8aa8e42 100644 --- a/ui/tests/integration/components/pki/pki-generate-root-test.js +++ b/ui/tests/integration/components/pki/pki-generate-root-test.js @@ -144,7 +144,7 @@ module('Integration | Component | pki-generate-root', function (hooks) { assert .dom(SELECTORS.toggleGroupDescription) .hasText( - 'This certificate type is kms, meaning managed keys will be used. Below, you will name the key and tell OpenBao where to find it in your KMS or HSM. Learn more about managed keys.', + 'This certificate type is kms, meaning managed keys will be used. Below, you will name the key and tell OpenBao where to find it in your KMS or HSM.', `has correct description for type=${this.type}` ); assert.strictEqual(this.model.type, this.type); diff --git a/ui/tests/integration/components/pki/pki-tidy-form-test.js b/ui/tests/integration/components/pki/pki-tidy-form-test.js index 3eadc2aeb8..7138c01076 100644 --- a/ui/tests/integration/components/pki/pki-tidy-form-test.js +++ b/ui/tests/integration/components/pki/pki-tidy-form-test.js @@ -80,99 +80,6 @@ module('Integration | Component | pki tidy form', function (hooks) { }); }); - test('it renders all attribute fields, including enterprise', async function (assert) { - assert.expect(25); - this.version.version = '1.14.1+ent'; - this.autoTidy.enabled = true; - const skipFields = ['enabled', 'tidyAcme', 'intervalDuration']; // combined with duration ttl or asserted separately - await render( - hbs` - - `, - { owner: this.engine } - ); - - this.autoTidy.eachAttribute((attr) => { - if (skipFields.includes(attr)) return; - assert.dom(SELECTORS.inputByAttr(attr)).exists(`renders ${attr} for auto tidyType`); - }); - - await render( - hbs` - - `, - { owner: this.engine } - ); - assert.dom(SELECTORS.toggleInput('intervalDuration')).doesNotExist('hides automatic tidy toggle'); - - this.manualTidy.eachAttribute((attr) => { - if (skipFields.includes(attr)) return; - assert.dom(SELECTORS.inputByAttr(attr)).exists(`renders ${attr} for manual tidyType`); - }); - }); - - test('it hides enterprise fields for OSS', async function (assert) { - assert.expect(7); - this.version.version = '1.14.1'; - this.autoTidy.enabled = true; - - const enterpriseFields = [ - 'tidyRevocationQueue', - 'tidyCrossClusterRevokedCerts', - 'revocationQueueSafetyBuffer', - ]; - - // tidyType = auto - await render( - hbs` - - `, - { owner: this.engine } - ); - - assert - .dom(SELECTORS.tidySectionHeader('Cross-cluster operations')) - .doesNotExist(`does not render ent header`); - - enterpriseFields.forEach((entAttr) => { - assert.dom(SELECTORS.inputByAttr(entAttr)).doesNotExist(`does not render ${entAttr} for auto tidyType`); - }); - - // tidyType = manual - await render( - hbs` - - `, - { owner: this.engine } - ); - - enterpriseFields.forEach((entAttr) => { - assert - .dom(SELECTORS.inputByAttr(entAttr)) - .doesNotExist(`does not render ${entAttr} for manual tidyType`); - }); - }); - test('it should change the attributes on the model', async function (assert) { assert.expect(12); this.server.post('/pki-auto-tidy/config/auto-tidy', (schema, req) => { diff --git a/ui/tests/integration/components/shamir-modal-flow-test.js b/ui/tests/integration/components/shamir-modal-flow-test.js index 8a3ea6b696..e69de29bb2 100644 --- a/ui/tests/integration/components/shamir-modal-flow-test.js +++ b/ui/tests/integration/components/shamir-modal-flow-test.js @@ -1,87 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { module, test, skip } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; -import sinon from 'sinon'; -import { setupMirage } from 'ember-cli-mirage/test-support'; - -module('Integration | Component | shamir-modal-flow', function (hooks) { - setupRenderingTest(hooks); - setupMirage(hooks); - - hooks.beforeEach(function () { - this.set('isActive', true); - this.set('onClose', sinon.spy()); - }); - - test('it renders with initial content by default', async function (assert) { - await render(hbs` - - -

Inner content goes here

-
- `); - - assert - .dom('[data-test-shamir-modal-body]') - .hasText('Inner content goes here', 'Template block gets rendered'); - assert.dom('[data-test-shamir-modal-cancel-button]').hasText('Cancel', 'Shows cancel button'); - }); - - test('Shows correct content when started', async function (assert) { - await render(hbs` - - -

Inner content goes here

-
- `); - assert.dom('[data-test-shamir-input]').exists('Asks for root key Portion'); - assert.dom('[data-test-shamir-modal-cancel-button]').hasText('Cancel', 'Shows cancel button'); - }); - - test('Shows OTP when provided and flow started', async function (assert) { - await render(hbs` - - -

Inner content goes here

-
- `); - assert.dom('[data-test-shamir-encoded-token]').hasText('my-encoded-token', 'Shows encoded token'); - assert.dom('[data-test-shamir-modal-cancel-button]').hasText('Close', 'Shows close button'); - }); - skip('DR Secondary actions', async function () { - // DR Secondaries cannot be tested yet, but once they can - // we should add tests for Cancel button functionality - }); -}); diff --git a/ui/tests/integration/components/sidebar/frame-test.js b/ui/tests/integration/components/sidebar/frame-test.js index aa6bd41622..ff0ba8382d 100644 --- a/ui/tests/integration/components/sidebar/frame-test.js +++ b/ui/tests/integration/components/sidebar/frame-test.js @@ -2,7 +2,6 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'ember-qunit'; import { render, click } from '@ember/test-helpers'; import hbs from 'htmlbars-inline-precompile'; -import sinon from 'sinon'; module('Integration | Component | sidebar-frame', function (hooks) { setupRenderingTest(hooks); @@ -51,17 +50,4 @@ module('Integration | Component | sidebar-frame', function (hooks) { assert.dom('.panel-open').doesNotExist('Console ui panel closes'); assert.dom('[data-test-user-menu]').exists('User menu renders'); }); - - test('it should render namespace picker in sidebar footer', async function (assert) { - const version = this.owner.lookup('service:version'); - version.features = ['Namespaces']; - const auth = this.owner.lookup('service:auth'); - sinon.stub(auth, 'authData').value({}); - - await render(hbs` - - `); - - assert.dom('.namespace-picker').exists('Namespace picker renders in sidebar footer'); - }); }); diff --git a/ui/tests/integration/components/sidebar/nav/access-test.js b/ui/tests/integration/components/sidebar/nav/access-test.js index 650b6887c9..4021b333a4 100644 --- a/ui/tests/integration/components/sidebar/nav/access-test.js +++ b/ui/tests/integration/components/sidebar/nav/access-test.js @@ -16,7 +16,7 @@ module('Integration | Component | sidebar-nav-access', function (hooks) { setupRenderingTest(hooks); test('it should render nav headings', async function (assert) { - const headings = ['Authentication', 'Access Control', 'Organization', 'Administration']; + const headings = ['Authentication', 'Organization', 'Administration']; stubFeaturesAndPermissions(this.owner); await renderComponent(); @@ -47,8 +47,6 @@ module('Integration | Component | sidebar-nav-access', function (hooks) { 'Authentication methods', 'Multi-factor authentication', 'OIDC provider', - 'Control Groups', - 'Namespaces', 'Groups', 'Entities', 'Leases', diff --git a/ui/tests/integration/components/sidebar/nav/cluster-test.js b/ui/tests/integration/components/sidebar/nav/cluster-test.js index ae3f629d6e..5688ad833e 100644 --- a/ui/tests/integration/components/sidebar/nav/cluster-test.js +++ b/ui/tests/integration/components/sidebar/nav/cluster-test.js @@ -41,15 +41,7 @@ module('Integration | Component | sidebar-nav-cluster', function (hooks) { }); test('it should render nav links', async function (assert) { - const links = [ - 'Secrets engines', - 'Access', - 'Policies', - 'Tools', - 'Raft Storage', - 'License', - 'Seal OpenBao', - ]; + const links = ['Secrets engines', 'Access', 'Policies', 'Tools', 'Raft Storage', 'Seal OpenBao']; stubFeaturesAndPermissions(this.owner, true, true); await renderComponent(); @@ -60,25 +52,4 @@ module('Integration | Component | sidebar-nav-cluster', function (hooks) { assert.dom(`[data-test-sidebar-nav-link="${link}"]`).hasText(link, `${link} link renders`); }); }); - - test('it should hide enterprise related links in child namespace', async function (assert) { - const links = ['Raft Storage', 'License', 'Seal OpenBao']; - this.owner.lookup('service:namespace').set('path', 'foo'); - const stubs = stubFeaturesAndPermissions(this.owner, true, true); - stubs.hasNavPermission.callsFake((route) => route !== 'clients'); - - await renderComponent(); - - assert - .dom('[data-test-sidebar-nav-heading="Monitoring"]') - .doesNotExist( - 'Monitoring heading is hidden in child namespace when user does not have access to Client Count' - ); - - links.forEach((link) => { - assert - .dom(`[data-test-sidebar-nav-link="${link}"]`) - .doesNotExist(`${link} is hidden in child namespace`); - }); - }); }); diff --git a/ui/tests/integration/components/sidebar/nav/policies-test.js b/ui/tests/integration/components/sidebar/nav/policies-test.js index b97a55bcc9..c3fe3241a3 100644 --- a/ui/tests/integration/components/sidebar/nav/policies-test.js +++ b/ui/tests/integration/components/sidebar/nav/policies-test.js @@ -23,12 +23,7 @@ module('Integration | Component | sidebar-nav-policies', function (hooks) { }); test('it should render nav headings and links', async function (assert) { - const links = [ - 'Back to main navigation', - 'ACL Policies', - 'Role-Governing Policies', - 'Endpoint Governing Policies', - ]; + const links = ['Back to main navigation', 'ACL Policies']; stubFeaturesAndPermissions(this.owner); await renderComponent(); diff --git a/ui/tests/integration/helpers/changelog-url-for-test.js b/ui/tests/integration/helpers/changelog-url-for-test.js index 073a814638..b52ebf6c20 100644 --- a/ui/tests/integration/helpers/changelog-url-for-test.js +++ b/ui/tests/integration/helpers/changelog-url-for-test.js @@ -11,12 +11,6 @@ const CHANGELOG_URL = 'https://www.github.com/openbao/openbao/blob/main/CHANGELO module('Integration | Helper | changelog-url-for', function (hooks) { setupRenderingTest(hooks); - - test('it builds an enterprise URL', function (assert) { - const result = changelogUrlFor(['1.5.0+prem']); - assert.strictEqual(result, CHANGELOG_URL.concat('150')); - }); - test('it builds an OSS URL', function (assert) { const result = changelogUrlFor(['1.4.3']); assert.strictEqual(result, CHANGELOG_URL.concat('143')); diff --git a/ui/tests/integration/services/auth-test.js b/ui/tests/integration/services/auth-test.js index e78ee0ee92..130b7ccb62 100644 --- a/ui/tests/integration/services/auth-test.js +++ b/ui/tests/integration/services/auth-test.js @@ -253,35 +253,6 @@ module('Integration | Service | auth', function (hooks) { assert.strictEqual(this.memStore.keys().length, 0, 'mem storage is empty'); }); - test('github authentication', function (assert) { - assert.expect(6); - const done = assert.async(); - const service = this.owner.factoryFor('service:auth').create({ - storage: (type) => (type === 'memory' ? this.memStore : this.store), - }); - - run(() => { - service.authenticate({ clusterId: '1', backend: 'github', data: { token: 'test' } }).then(() => { - const clusterTokenName = service.get('currentTokenName'); - const clusterToken = service.get('currentToken'); - const authData = service.get('authData'); - const expectedTokenName = `${TOKEN_PREFIX}github${TOKEN_SEPARATOR}1`; - - assert.strictEqual(GITHUB_RESPONSE.auth.client_token, clusterToken, 'token is saved properly'); - assert.strictEqual(expectedTokenName, clusterTokenName, 'token name is saved properly'); - assert.strictEqual(authData.backend.type, 'github', 'backend is saved properly'); - assert.strictEqual( - GITHUB_RESPONSE.auth.metadata.org + '/' + GITHUB_RESPONSE.auth.metadata.username, - authData.displayName, - 'displayName is saved properly' - ); - assert.strictEqual(this.memStore.keys().length, 0, 'mem storage is empty'); - assert.ok(this.store.keys().includes(expectedTokenName), 'normal storage contains the token'); - done(); - }); - }); - }); - test('userpass authentication', function (assert) { assert.expect(4); const done = assert.async(); diff --git a/ui/tests/unit/adapters/clients-activity-test.js b/ui/tests/unit/adapters/clients-activity-test.js deleted file mode 100644 index 4d37241124..0000000000 --- a/ui/tests/unit/adapters/clients-activity-test.js +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { module, test } from 'qunit'; -import sinon from 'sinon'; -import { setupTest } from 'ember-qunit'; -import { setupMirage } from 'ember-cli-mirage/test-support'; -import { subMonths, fromUnixTime, addMonths } from 'date-fns'; -import { parseAPITimestamp } from 'core/utils/date-formatters'; -import timestamp from 'core/utils/timestamp'; - -module('Unit | Adapter | clients activity', function (hooks) { - setupTest(hooks); - setupMirage(hooks); - - hooks.before(function () { - sinon.stub(timestamp, 'now').callsFake(() => new Date('2023-01-13T09:30:15')); - }); - hooks.beforeEach(function () { - this.store = this.owner.lookup('service:store'); - this.modelName = 'clients/activity'; - this.startDate = subMonths(timestamp.now(), 6); - this.endDate = timestamp.now(); - this.readableUnix = (unix) => parseAPITimestamp(fromUnixTime(unix).toISOString(), 'MMMM dd yyyy'); - }); - hooks.after(function () { - timestamp.now.restore(); - }); - - test('it does not format if both params are timestamp strings', async function (assert) { - assert.expect(1); - const queryParams = { - start_time: { timestamp: this.startDate.toISOString() }, - end_time: { timestamp: this.endDate.toISOString() }, - }; - this.server.get('sys/internal/counters/activity', (schema, req) => { - assert.propEqual(req.queryParams, { - start_time: this.startDate.toISOString(), - end_time: this.endDate.toISOString(), - }); - }); - - this.store.queryRecord(this.modelName, queryParams); - }); - - test('it formats start_time if only end_time is a timestamp string', async function (assert) { - assert.expect(2); - const twoMonthsAhead = addMonths(this.startDate, 2); - const month = twoMonthsAhead.getMonth(); - const year = twoMonthsAhead.getFullYear(); - const queryParams = { - start_time: { - monthIdx: month, - year, - }, - end_time: { - timestamp: this.endDate.toISOString(), - }, - }; - - this.server.get('sys/internal/counters/activity', (schema, req) => { - const { start_time, end_time } = req.queryParams; - const readableStart = this.readableUnix(start_time); - assert.strictEqual( - readableStart, - `September 01 2022`, - `formatted unix start time is the first of the month: ${readableStart}` - ); - assert.strictEqual(end_time, this.endDate.toISOString(), 'end time is a timestamp string'); - }); - this.store.queryRecord(this.modelName, queryParams); - }); - - test('it formats end_time only if only start_time is a timestamp string', async function (assert) { - assert.expect(2); - const twoMothsAgo = subMonths(this.endDate, 2); - const endMonth = twoMothsAgo.getMonth(); - const year = twoMothsAgo.getFullYear(); - const queryParams = { - start_time: { - timestamp: this.startDate.toISOString(), - }, - end_time: { - monthIdx: endMonth, - year, - }, - }; - - this.server.get('sys/internal/counters/activity', (schema, req) => { - const { start_time, end_time } = req.queryParams; - const readableEnd = this.readableUnix(end_time); - assert.strictEqual(start_time, this.startDate.toISOString(), 'start time is a timestamp string'); - assert.strictEqual( - readableEnd, - `November 30 2022`, - `formatted unix end time is the last day of the month: ${readableEnd}` - ); - }); - - this.store.queryRecord(this.modelName, queryParams); - }); - - test('it formats both params if neither are a timestamp', async function (assert) { - assert.expect(2); - const startDate = subMonths(this.startDate, 2); - const endDate = addMonths(this.endDate, 2); - const startMonth = startDate.getMonth(); - const startYear = startDate.getFullYear(); - const endMonth = endDate.getMonth(); - const endYear = endDate.getFullYear(); - const queryParams = { - start_time: { - monthIdx: startMonth, - year: startYear, - }, - end_time: { - monthIdx: endMonth, - year: endYear, - }, - }; - - this.server.get('sys/internal/counters/activity', (schema, req) => { - const { start_time, end_time } = req.queryParams; - const readableEnd = this.readableUnix(end_time); - const readableStart = this.readableUnix(start_time); - assert.strictEqual( - readableStart, - `May 01 2022`, - `formatted unix start time is the first of the month: ${readableStart}` - ); - assert.strictEqual( - readableEnd, - `March 31 2023`, - `formatted unix end time is the last day of the month: ${readableEnd}` - ); - }); - - this.store.queryRecord(this.modelName, queryParams); - }); -}); diff --git a/ui/tests/unit/adapters/cluster-test.js b/ui/tests/unit/adapters/cluster-test.js index 6d75081b9d..e69de29bb2 100644 --- a/ui/tests/unit/adapters/cluster-test.js +++ b/ui/tests/unit/adapters/cluster-test.js @@ -1,145 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { resolve } from 'rsvp'; -import { module, test } from 'qunit'; -import { setupTest } from 'ember-qunit'; - -module('Unit | Adapter | cluster', function (hooks) { - setupTest(hooks); - - test('cluster api urls', function (assert) { - let url, method, options; - const adapter = this.owner.factoryFor('adapter:cluster').create({ - ajax: (...args) => { - [url, method, options] = args; - return resolve(); - }, - }); - adapter.health(); - assert.strictEqual(url, '/v1/sys/health', 'health url OK'); - assert.deepEqual( - { - standbycode: 200, - sealedcode: 200, - uninitcode: 200, - drsecondarycode: 200, - performancestandbycode: 200, - }, - options.data, - 'health data params OK' - ); - assert.strictEqual(method, 'GET', 'health method OK'); - - adapter.sealStatus(); - assert.strictEqual(url, '/v1/sys/seal-status', 'health url OK'); - assert.strictEqual(method, 'GET', 'seal-status method OK'); - - let data = { someData: 1 }; - adapter.unseal(data); - assert.strictEqual(url, '/v1/sys/unseal', 'unseal url OK'); - assert.strictEqual(method, 'PUT', 'unseal method OK'); - assert.deepEqual({ data, unauthenticated: true }, options, 'unseal options OK'); - - adapter.initCluster(data); - assert.strictEqual(url, '/v1/sys/init', 'init url OK'); - assert.strictEqual(method, 'PUT', 'init method OK'); - assert.deepEqual({ data, unauthenticated: true }, options, 'init options OK'); - - data = { token: 'token', password: 'password', username: 'username' }; - - adapter.authenticate({ backend: 'token', data }); - assert.strictEqual(url, '/v1/auth/token/lookup-self', 'auth:token url OK'); - assert.strictEqual(method, 'GET', 'auth:token method OK'); - assert.deepEqual( - { headers: { 'X-Vault-Token': 'token' }, unauthenticated: true }, - options, - 'auth:token options OK' - ); - - adapter.authenticate({ backend: 'github', data }); - assert.strictEqual(url, '/v1/auth/github/login', 'auth:github url OK'); - assert.strictEqual(method, 'POST', 'auth:github method OK'); - assert.deepEqual( - { data: { password: 'password', token: 'token' }, unauthenticated: true }, - options, - 'auth:github options OK' - ); - - data = { jwt: 'token', role: 'test' }; - adapter.authenticate({ backend: 'jwt', data }); - assert.strictEqual(url, '/v1/auth/jwt/login', 'auth:jwt url OK'); - assert.strictEqual(method, 'POST', 'auth:jwt method OK'); - assert.deepEqual( - { data: { jwt: 'token', role: 'test' }, unauthenticated: true }, - options, - 'auth:jwt options OK' - ); - - data = { jwt: 'token', role: 'test', path: 'oidc' }; - adapter.authenticate({ backend: 'jwt', data }); - assert.strictEqual(url, '/v1/auth/oidc/login', 'auth:jwt custom mount path, url OK'); - - data = { token: 'token', password: 'password', username: 'username', path: 'path' }; - - adapter.authenticate({ backend: 'token', data }); - assert.strictEqual(url, '/v1/auth/token/lookup-self', 'auth:token url with path OK'); - - adapter.authenticate({ backend: 'github', data }); - assert.strictEqual(url, '/v1/auth/path/login', 'auth:github with path url OK'); - - data = { password: 'password', username: 'username' }; - - adapter.authenticate({ backend: 'userpass', data }); - assert.strictEqual(url, '/v1/auth/userpass/login/username', 'auth:userpass url OK'); - assert.strictEqual(method, 'POST', 'auth:userpass method OK'); - assert.deepEqual( - { data: { password: 'password' }, unauthenticated: true }, - options, - 'auth:userpass options OK' - ); - - adapter.authenticate({ backend: 'radius', data }); - assert.strictEqual(url, '/v1/auth/radius/login/username', 'auth:RADIUS url OK'); - assert.strictEqual(method, 'POST', 'auth:RADIUS method OK'); - assert.deepEqual( - { data: { password: 'password' }, unauthenticated: true }, - options, - 'auth:RADIUS options OK' - ); - - adapter.authenticate({ backend: 'LDAP', data }); - assert.strictEqual(url, '/v1/auth/ldap/login/username', 'ldap:userpass url OK'); - assert.strictEqual(method, 'POST', 'ldap:userpass method OK'); - assert.deepEqual( - { data: { password: 'password' }, unauthenticated: true }, - options, - 'ldap:userpass options OK' - ); - - data = { password: 'password', username: 'username', nonce: 'uuid' }; - adapter.authenticate({ backend: 'okta', data }); - assert.strictEqual(url, '/v1/auth/okta/login/username', 'okta:userpass url OK'); - assert.strictEqual(method, 'POST', 'ldap:userpass method OK'); - assert.deepEqual( - { data: { password: 'password', nonce: 'uuid' }, unauthenticated: true }, - options, - 'okta:userpass options OK' - ); - - // use a custom mount path - data = { password: 'password', username: 'username', path: 'path' }; - - adapter.authenticate({ backend: 'userpass', data }); - assert.strictEqual(url, '/v1/auth/path/login/username', 'auth:userpass with path url OK'); - - adapter.authenticate({ backend: 'LDAP', data }); - assert.strictEqual(url, '/v1/auth/path/login/username', 'auth:LDAP with path url OK'); - - data = { password: 'password', username: 'username', path: 'path', nonce: 'uuid' }; - adapter.authenticate({ backend: 'Okta', data }); - assert.strictEqual(url, '/v1/auth/path/login/username', 'auth:Okta with path url OK'); - }); -}); diff --git a/ui/tests/unit/machines/secrets-machine-test.js b/ui/tests/unit/machines/secrets-machine-test.js index 742d6c1249..df209a93bc 100644 --- a/ui/tests/unit/machines/secrets-machine-test.js +++ b/ui/tests/unit/machines/secrets-machine-test.js @@ -23,127 +23,6 @@ module('Unit | Machine | secrets-machine', function () { ], }, }, - { - currentState: 'enable', - event: 'CONTINUE', - params: 'aws', - expectedResults: { - value: 'details', - actions: [ - { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, - { component: 'wizard/secrets-details', level: 'step', type: 'render' }, - ], - }, - }, - { - currentState: 'details', - event: 'CONTINUE', - params: 'aws', - expectedResults: { - value: 'role', - actions: [ - { component: 'wizard/secrets-role', level: 'step', type: 'render' }, - { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, - ], - }, - }, - { - currentState: 'role', - event: 'CONTINUE', - params: 'aws', - expectedResults: { - value: 'displayRole', - actions: [ - { component: 'wizard/secrets-display-role', level: 'step', type: 'render' }, - { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, - ], - }, - }, - { - currentState: 'displayRole', - event: 'CONTINUE', - params: 'aws', - expectedResults: { - value: 'credentials', - actions: [ - { component: 'wizard/secrets-credentials', level: 'step', type: 'render' }, - { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, - ], - }, - }, - { - currentState: 'credentials', - event: 'CONTINUE', - params: 'aws', - expectedResults: { - value: 'display', - actions: [ - { component: 'wizard/secrets-display', level: 'step', type: 'render' }, - { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, - ], - }, - }, - { - currentState: 'display', - event: 'REPEAT', - params: 'aws', - expectedResults: { - value: 'role', - actions: [ - { - params: ['vault.cluster.secrets.backend.create-root'], - type: 'routeTransition', - }, - { component: 'wizard/secrets-role', level: 'step', type: 'render' }, - { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, - ], - }, - }, - { - currentState: 'display', - event: 'RESET', - params: 'aws', - expectedResults: { - value: 'idle', - actions: [ - { - params: ['vault.cluster.settings.mount-secret-backend'], - type: 'routeTransition', - }, - { - component: 'wizard/mounts-wizard', - level: 'feature', - type: 'render', - }, - { - component: 'wizard/secrets-idle', - level: 'step', - type: 'render', - }, - ], - }, - }, - { - currentState: 'display', - event: 'DONE', - params: 'aws', - expectedResults: { - value: 'complete', - actions: ['completeFeature'], - }, - }, - { - currentState: 'display', - event: 'ERROR', - params: 'aws', - expectedResults: { - value: 'error', - actions: [ - { component: 'wizard/tutorial-error', level: 'step', type: 'render' }, - { component: 'wizard/mounts-wizard', level: 'feature', type: 'render' }, - ], - }, - }, { currentState: 'enable', event: 'CONTINUE', diff --git a/ui/tests/unit/mixins/cluster-route-test.js b/ui/tests/unit/mixins/cluster-route-test.js deleted file mode 100644 index fb64061fca..0000000000 --- a/ui/tests/unit/mixins/cluster-route-test.js +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { assign } from '@ember/polyfills'; -import EmberObject from '@ember/object'; -import ClusterRouteMixin from 'vault/mixins/cluster-route'; -import { INIT, UNSEAL, AUTH, CLUSTER, CLUSTER_INDEX, REDIRECT } from 'vault/lib/route-paths'; -import { module, test } from 'qunit'; -import sinon from 'sinon'; - -module('Unit | Mixin | cluster route', function () { - function createClusterRoute( - clusterModel = {}, - methods = { router: {}, hasKeyData: () => false, authToken: () => null, transitionTo: () => {} } - ) { - const ClusterRouteObject = EmberObject.extend( - ClusterRouteMixin, - assign(methods, { clusterModel: () => clusterModel }) - ); - return ClusterRouteObject.create(); - } - - test('#targetRouteName init', function (assert) { - let subject = createClusterRoute({ needsInit: true }); - subject.routeName = CLUSTER; - assert.strictEqual(subject.targetRouteName(), INIT, 'forwards to INIT when cluster needs init'); - - subject = createClusterRoute({ needsInit: false, sealed: true }); - subject.routeName = CLUSTER; - assert.strictEqual(subject.targetRouteName(), UNSEAL, 'forwards to UNSEAL if sealed and initialized'); - - subject = createClusterRoute({ needsInit: false }); - subject.routeName = CLUSTER; - assert.strictEqual(subject.targetRouteName(), AUTH, 'forwards to AUTH if unsealed and initialized'); - - subject = createClusterRoute({ dr: { isSecondary: true } }); - subject.routeName = CLUSTER; - assert.strictEqual( - subject.targetRouteName(), - 'forwards to DR_REPLICATION_SECONDARY if is a dr secondary' - ); - }); - - test('#targetRouteName when #hasDataKey is true', function (assert) { - let subject = createClusterRoute( - { needsInit: false, sealed: true }, - { hasKeyData: () => true, authToken: () => null } - ); - - subject.routeName = CLUSTER; - assert.strictEqual( - subject.targetRouteName(), - INIT, - 'still land on INIT if there are keys on the controller' - ); - - subject.routeName = UNSEAL; - assert.strictEqual(subject.targetRouteName(), UNSEAL, 'allowed to proceed to unseal'); - - subject = createClusterRoute( - { needsInit: false, sealed: false }, - { hasKeyData: () => true, authToken: () => null } - ); - - subject.routeName = AUTH; - assert.strictEqual(subject.targetRouteName(), AUTH, 'allowed to proceed to auth'); - }); - - test('#targetRouteName happy path forwards to CLUSTER route', function (assert) { - const subject = createClusterRoute( - { needsInit: false, sealed: false, dr: { isSecondary: false } }, - { hasKeyData: () => false, authToken: () => 'a token' } - ); - subject.routeName = INIT; - assert.strictEqual(subject.targetRouteName(), CLUSTER, 'forwards when inited and navigating to INIT'); - - subject.routeName = UNSEAL; - assert.strictEqual(subject.targetRouteName(), CLUSTER, 'forwards when unsealed and navigating to UNSEAL'); - - subject.routeName = AUTH; - assert.strictEqual( - subject.targetRouteName(), - REDIRECT, - 'forwards when authenticated and navigating to AUTH' - ); - }); - - test('#targetRouteName happy path when not authed forwards to AUTH', function (assert) { - const subject = createClusterRoute( - { needsInit: false, sealed: false, dr: { isSecondary: false } }, - { hasKeyData: () => false, authToken: () => null } - ); - subject.routeName = INIT; - assert.strictEqual(subject.targetRouteName(), AUTH, 'forwards when inited and navigating to INIT'); - - subject.routeName = UNSEAL; - assert.strictEqual(subject.targetRouteName(), AUTH, 'forwards when unsealed and navigating to UNSEAL'); - - subject.routeName = AUTH; - assert.strictEqual( - subject.targetRouteName(), - AUTH, - 'forwards when non-authenticated and navigating to AUTH' - ); - }); - - test('#transitionToTargetRoute', function (assert) { - const redirectRouteURL = '/vault/secrets/secret/create'; - const subject = createClusterRoute({ needsInit: false, sealed: false }); - subject.router.currentURL = redirectRouteURL; - const spy = sinon.spy(subject, 'transitionTo'); - subject.transitionToTargetRoute(); - assert.ok( - spy.calledWithExactly(AUTH, { queryParams: { redirect_to: redirectRouteURL } }), - 'calls transitionTo with the expected args' - ); - - spy.restore(); - }); - - test('#transitionToTargetRoute with auth as a target', function (assert) { - const subject = createClusterRoute({ needsInit: false, sealed: false }); - const spy = sinon.spy(subject, 'transitionTo'); - // in this case it's already transitioning to the AUTH route so we don't need to call transitionTo again - subject.transitionToTargetRoute({ targetName: AUTH }); - assert.ok(spy.notCalled, 'transitionTo is not called'); - spy.restore(); - }); - - test('#transitionToTargetRoute with auth target, coming from cluster route', function (assert) { - const subject = createClusterRoute({ needsInit: false, sealed: false }); - const spy = sinon.spy(subject, 'transitionTo'); - subject.transitionToTargetRoute({ targetName: CLUSTER_INDEX }); - assert.ok(spy.calledWithExactly(AUTH), 'calls transitionTo without redirect_to'); - spy.restore(); - }); -}); diff --git a/ui/tests/unit/services/permissions-test.js b/ui/tests/unit/services/permissions-test.js index f40ec85d5a..133a6aec53 100644 --- a/ui/tests/unit/services/permissions-test.js +++ b/ui/tests/unit/services/permissions-test.js @@ -124,14 +124,11 @@ module('Unit | Service | permissions', function (hooks) { test('returns the first allowed nav route for policies', function (assert) { const policyPaths = { 'sys/policies/acl': { - capabilities: ['deny'], - }, - 'sys/policies/rgp': { capabilities: ['read'], }, }; this.service.set('exactPaths', policyPaths); - assert.strictEqual(this.service.navPathParams('policies').models[0], 'rgp'); + assert.strictEqual(this.service.navPathParams('policies').models[0], 'acl'); }); test('returns the first allowed nav route for access', function (assert) { diff --git a/ui/tests/unit/services/version-test.js b/ui/tests/unit/services/version-test.js index d1ee29aabb..12a4aee096 100644 --- a/ui/tests/unit/services/version-test.js +++ b/ui/tests/unit/services/version-test.js @@ -15,18 +15,4 @@ module('Unit | Service | version', function (hooks) { assert.true(service.isOSS); assert.false(service.isEnterprise); }); - - test('setting version computes isEnterprise properly', function (assert) { - const service = this.owner.lookup('service:version'); - service.version = '0.9.5+ent'; - assert.false(service.isOSS); - assert.true(service.isEnterprise); - }); - - test('setting version with hsm ending computes isEnterprise properly', function (assert) { - const service = this.owner.lookup('service:version'); - service.version = '0.9.5+ent.hsm'; - assert.false(service.isOSS); - assert.true(service.isEnterprise); - }); }); diff --git a/ui/tests/unit/utils/decode-config-from-jwt-test.js b/ui/tests/unit/utils/decode-config-from-jwt-test.js deleted file mode 100644 index 999991ce4d..0000000000 --- a/ui/tests/unit/utils/decode-config-from-jwt-test.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import decodeConfigFromJWT from 'replication/utils/decode-config-from-jwt'; -import { module, test } from 'qunit'; - -module('Unit | Util | decode config from jwt', function () { - const PADDING_STRIPPED_TOKEN = - 'eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJhZGRyIjoiaHR0cDovLzE5Mi4xNjguNTAuMTUwOjgyMDAiLCJleHAiOjE1MTczNjkwNzUsImlhdCI6MTUxNzM2NzI3NSwianRpIjoiN2IxZDZkZGUtZmViZC00ZGU1LTc0MWUtZDU2ZTg0ZTNjZDk2IiwidHlwZSI6IndyYXBwaW5nIn0.MIGIAkIB6s2zbohbxLimwhM6cg16OISK2DgoTgy1vHbTjPT8uG4hsrJndZp5COB8dX-djWjx78ZFMk-3a6Ij51su_By9xsoCQgFXV8y3DzH_YzYvdL9x38dMSWaVHpR_lpoKWsQnMvAukSchJp1FfHZQ8JcSkPu5IAVZdfwlG5esJ_ZOMxA3KIQFnA'; - const NO_PADDING_TOKEN = - 'eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJhZGRyIjoiaHR0cDovLzEyNy4wLjAuMTo4MjAwIiwiZXhwIjoxNTE3NDM0NDA2LCJpYXQiOjE1MTc0MzI2MDYsImp0aSI6IjBiYmI1ZWMyLWM0ODgtMzRjYi0wMzY5LTkxZmJiMjVkZTFiYSIsInR5cGUiOiJ3cmFwcGluZyJ9.MIGHAkIBAGzB5EW6PolAi2rYOzZNvfJnR902WxprtRqnSF2E2I2ye9XLGX--L7npSBjBhnd27ocQ4ZO9VhfDIFqMzu1TNiwCQT52O6xAoz9ElRrq76PjkEHO4ns5_ZgjSKXuKaqdGysHYSlry8KEjWLGQECvZWg9LQeIf35jwqeQUfyJUfmwl5r_'; - const INVALID_JSON_TOKEN = `foo.${btoa({ addr: 'http://127.0.0.1' })}.bar`; - - test('it decodes token with no padding', function (assert) { - const config = decodeConfigFromJWT(NO_PADDING_TOKEN); - - assert.ok(!!config, 'config was decoded'); - assert.ok(!!config.addr, 'config.addr is present'); - }); - - test('it decodes token with stripped padding', function (assert) { - const config = decodeConfigFromJWT(PADDING_STRIPPED_TOKEN); - - assert.ok(!!config, 'config was decoded'); - assert.ok(!!config.addr, 'config.addr is present'); - }); - - test('it returns nothing if the config is invalid JSON', function (assert) { - const config = decodeConfigFromJWT(INVALID_JSON_TOKEN); - - assert.notOk(config, 'config is not present'); - }); -}); diff --git a/ui/yarn.lock b/ui/yarn.lock index b5e3a4d601..90ae52603b 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -14,6 +14,16 @@ __metadata: languageName: node linkType: hard +"@ampproject/remapping@npm:^2.2.0": + version: 2.3.0 + resolution: "@ampproject/remapping@npm:2.3.0" + dependencies: + "@jridgewell/gen-mapping": ^0.3.5 + "@jridgewell/trace-mapping": ^0.3.24 + checksum: d3ad7b89d973df059c4e8e6d7c972cbeb1bb2f18f002a3bd04ae0707da214cb06cc06929b65aa2313b9347463df2914772298bae8b1d7973f246bb3f2ab3e8f0 + languageName: node + linkType: hard + "@babel/code-frame@npm:7.12.11": version: 7.12.11 resolution: "@babel/code-frame@npm:7.12.11" @@ -59,6 +69,17 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.0, @babel/code-frame@npm:^7.26.2": + version: 7.26.2 + resolution: "@babel/code-frame@npm:7.26.2" + dependencies: + "@babel/helper-validator-identifier": ^7.25.9 + js-tokens: ^4.0.0 + picocolors: ^1.0.0 + checksum: db13f5c42d54b76c1480916485e6900748bbcb0014a8aca87f50a091f70ff4e0d0a6db63cade75eb41fcc3d2b6ba0a7f89e343def4f96f00269b41b8ab8dd7b8 + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.13.11, @babel/compat-data@npm:^7.13.8, @babel/compat-data@npm:^7.14.0, @babel/compat-data@npm:^7.16.0": version: 7.16.4 resolution: "@babel/compat-data@npm:7.16.4" @@ -87,6 +108,13 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.25.9": + version: 7.26.3 + resolution: "@babel/compat-data@npm:7.26.3" + checksum: 85c5a9fb365231688c7faeb977f1d659da1c039e17b416f8ef11733f7aebe11fe330dce20c1844cacf243766c1d643d011df1d13cac9eda36c46c6c475693d21 + languageName: node + linkType: hard + "@babel/core@npm:^7.0.0, @babel/core@npm:^7.12.0, @babel/core@npm:^7.3.4": version: 7.16.0 resolution: "@babel/core@npm:7.16.0" @@ -156,7 +184,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.16.10, @babel/core@npm:^7.17.9": +"@babel/core@npm:^7.16.10": version: 7.19.0 resolution: "@babel/core@npm:7.19.0" dependencies: @@ -202,6 +230,29 @@ __metadata: languageName: node linkType: hard +"@babel/core@npm:^7.17.9": + version: 7.26.0 + resolution: "@babel/core@npm:7.26.0" + dependencies: + "@ampproject/remapping": ^2.2.0 + "@babel/code-frame": ^7.26.0 + "@babel/generator": ^7.26.0 + "@babel/helper-compilation-targets": ^7.25.9 + "@babel/helper-module-transforms": ^7.26.0 + "@babel/helpers": ^7.26.0 + "@babel/parser": ^7.26.0 + "@babel/template": ^7.25.9 + "@babel/traverse": ^7.25.9 + "@babel/types": ^7.26.0 + convert-source-map: ^2.0.0 + debug: ^4.1.0 + gensync: ^1.0.0-beta.2 + json5: ^2.2.3 + semver: ^6.3.1 + checksum: b296084cfd818bed8079526af93b5dfa0ba70282532d2132caf71d4060ab190ba26d3184832a45accd82c3c54016985a4109ab9118674347a7e5e9bc464894e6 + languageName: node + linkType: hard + "@babel/generator@npm:^7.14.0, @babel/generator@npm:^7.16.0": version: 7.16.0 resolution: "@babel/generator@npm:7.16.0" @@ -246,6 +297,19 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.26.0, @babel/generator@npm:^7.26.3": + version: 7.26.3 + resolution: "@babel/generator@npm:7.26.3" + dependencies: + "@babel/parser": ^7.26.3 + "@babel/types": ^7.26.3 + "@jridgewell/gen-mapping": ^0.3.5 + "@jridgewell/trace-mapping": ^0.3.25 + jsesc: ^3.0.2 + checksum: fb09fa55c66f272badf71c20a3a2cee0fa1a447fed32d1b84f16a668a42aff3e5f5ddc6ed5d832dda1e952187c002ca1a5cdd827022efe591b6ac44cada884ea + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.12.13, @babel/helper-annotate-as-pure@npm:^7.16.0": version: 7.16.0 resolution: "@babel/helper-annotate-as-pure@npm:7.16.0" @@ -349,6 +413,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-compilation-targets@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-compilation-targets@npm:7.25.9" + dependencies: + "@babel/compat-data": ^7.25.9 + "@babel/helper-validator-option": ^7.25.9 + browserslist: ^4.24.0 + lru-cache: ^5.1.1 + semver: ^6.3.1 + checksum: 3af536e2db358b38f968abdf7d512d425d1018fef2f485d6f131a57a7bcaed32c606b4e148bb230e1508fa42b5b2ac281855a68eb78270f54698c48a83201b9b + languageName: node + linkType: hard + "@babel/helper-create-class-features-plugin@npm:^7.13.0, @babel/helper-create-class-features-plugin@npm:^7.16.0, @babel/helper-create-class-features-plugin@npm:^7.8.3": version: 7.16.0 resolution: "@babel/helper-create-class-features-plugin@npm:7.16.0" @@ -722,6 +799,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-imports@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-module-imports@npm:7.25.9" + dependencies: + "@babel/traverse": ^7.25.9 + "@babel/types": ^7.25.9 + checksum: 1b411ce4ca825422ef7065dffae7d8acef52023e51ad096351e3e2c05837e9bf9fca2af9ca7f28dc26d596a588863d0fedd40711a88e350b736c619a80e704e6 + languageName: node + linkType: hard + "@babel/helper-module-transforms@npm:^7.14.0, @babel/helper-module-transforms@npm:^7.16.0": version: 7.16.0 resolution: "@babel/helper-module-transforms@npm:7.16.0" @@ -786,6 +873,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-transforms@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/helper-module-transforms@npm:7.26.0" + dependencies: + "@babel/helper-module-imports": ^7.25.9 + "@babel/helper-validator-identifier": ^7.25.9 + "@babel/traverse": ^7.25.9 + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 942eee3adf2b387443c247a2c190c17c4fd45ba92a23087abab4c804f40541790d51ad5277e4b5b1ed8d5ba5b62de73857446b7742f835c18ebd350384e63917 + languageName: node + linkType: hard + "@babel/helper-optimise-call-expression@npm:^7.12.13, @babel/helper-optimise-call-expression@npm:^7.16.0": version: 7.16.0 resolution: "@babel/helper-optimise-call-expression@npm:7.16.0" @@ -1000,6 +1100,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-string-parser@npm:7.25.9" + checksum: 6435ee0849e101681c1849868278b5aee82686ba2c1e27280e5e8aca6233af6810d39f8e4e693d2f2a44a3728a6ccfd66f72d71826a94105b86b731697cdfa99 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.14.0, @babel/helper-validator-identifier@npm:^7.15.7": version: 7.15.7 resolution: "@babel/helper-validator-identifier@npm:7.15.7" @@ -1028,6 +1135,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-validator-identifier@npm:7.25.9" + checksum: 5b85918cb1a92a7f3f508ea02699e8d2422fe17ea8e82acd445006c0ef7520fbf48e3dbcdaf7b0a1d571fc3a2715a29719e5226636cb6042e15fe6ed2a590944 + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.12.17, @babel/helper-validator-option@npm:^7.14.5": version: 7.14.5 resolution: "@babel/helper-validator-option@npm:7.14.5" @@ -1049,6 +1163,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-option@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-validator-option@npm:7.25.9" + checksum: 9491b2755948ebbdd68f87da907283698e663b5af2d2b1b02a2765761974b1120d5d8d49e9175b167f16f72748ffceec8c9cf62acfbee73f4904507b246e2b3d + languageName: node + linkType: hard + "@babel/helper-wrap-function@npm:^7.16.0": version: 7.16.0 resolution: "@babel/helper-wrap-function@npm:7.16.0" @@ -1117,6 +1238,16 @@ __metadata: languageName: node linkType: hard +"@babel/helpers@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/helpers@npm:7.26.0" + dependencies: + "@babel/template": ^7.25.9 + "@babel/types": ^7.26.0 + checksum: d77fe8d45033d6007eadfa440355c1355eed57902d5a302f450827ad3d530343430a21210584d32eef2f216ae463d4591184c6fc60cf205bbf3a884561469200 + languageName: node + linkType: hard + "@babel/highlight@npm:^7.10.4": version: 7.14.0 resolution: "@babel/highlight@npm:7.14.0" @@ -1206,6 +1337,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.25.9, @babel/parser@npm:^7.26.0, @babel/parser@npm:^7.26.3": + version: 7.26.3 + resolution: "@babel/parser@npm:7.26.3" + dependencies: + "@babel/types": ^7.26.3 + bin: + parser: ./bin/babel-parser.js + checksum: e2bff2e9fa6540ee18fecc058bc74837eda2ddcecbe13454667314a93fc0ba26c1fb862c812d84f6d5f225c3bd8d191c3a42d4296e287a882c4e1f82ff2815ff + languageName: node + linkType: hard + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.16.7": version: 7.16.7 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.16.7" @@ -3562,6 +3704,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/template@npm:7.25.9" + dependencies: + "@babel/code-frame": ^7.25.9 + "@babel/parser": ^7.25.9 + "@babel/types": ^7.25.9 + checksum: 103641fea19c7f4e82dc913aa6b6ac157112a96d7c724d513288f538b84bae04fb87b1f1e495ac1736367b1bc30e10f058b30208fb25f66038e1f1eb4e426472 + languageName: node + linkType: hard + "@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.12.1, @babel/traverse@npm:^7.4.5, @babel/traverse@npm:^7.7.0": version: 7.14.0 resolution: "@babel/traverse@npm:7.14.0" @@ -3649,6 +3802,21 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.25.9": + version: 7.26.4 + resolution: "@babel/traverse@npm:7.26.4" + dependencies: + "@babel/code-frame": ^7.26.2 + "@babel/generator": ^7.26.3 + "@babel/parser": ^7.26.3 + "@babel/template": ^7.25.9 + "@babel/types": ^7.26.3 + debug: ^4.3.1 + globals: ^11.1.0 + checksum: dcdf51b27ab640291f968e4477933465c2910bfdcbcff8f5315d1f29b8ff861864f363e84a71fb489f5e9708e8b36b7540608ce019aa5e57ef7a4ba537e62700 + languageName: node + linkType: hard + "@babel/types@npm:^7.1.6, @babel/types@npm:^7.7.0, @babel/types@npm:^7.7.2": version: 7.14.0 resolution: "@babel/types@npm:7.14.0" @@ -3711,6 +3879,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.25.9, @babel/types@npm:^7.26.0, @babel/types@npm:^7.26.3": + version: 7.26.3 + resolution: "@babel/types@npm:7.26.3" + dependencies: + "@babel/helper-string-parser": ^7.25.9 + "@babel/helper-validator-identifier": ^7.25.9 + checksum: 195f428080dcaadbcecc9445df7f91063beeaa91b49ccd78f38a5af6b75a6a58391d0c6614edb1ea322e57889a1684a0aab8e667951f820196901dd341f931e9 + languageName: node + linkType: hard + "@babel/types@npm:^7.8.3": version: 7.21.4 resolution: "@babel/types@npm:7.21.4" @@ -4692,6 +4870,17 @@ __metadata: languageName: node linkType: hard +"@jridgewell/gen-mapping@npm:^0.3.5": + version: 0.3.5 + resolution: "@jridgewell/gen-mapping@npm:0.3.5" + dependencies: + "@jridgewell/set-array": ^1.2.1 + "@jridgewell/sourcemap-codec": ^1.4.10 + "@jridgewell/trace-mapping": ^0.3.24 + checksum: ff7a1764ebd76a5e129c8890aa3e2f46045109dabde62b0b6c6a250152227647178ff2069ea234753a690d8f3c4ac8b5e7b267bbee272bffb7f3b0a370ab6e52 + languageName: node + linkType: hard + "@jridgewell/resolve-uri@npm:^3.0.3": version: 3.0.5 resolution: "@jridgewell/resolve-uri@npm:3.0.5" @@ -4699,6 +4888,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/resolve-uri@npm:^3.1.0": + version: 3.1.2 + resolution: "@jridgewell/resolve-uri@npm:3.1.2" + checksum: 83b85f72c59d1c080b4cbec0fef84528963a1b5db34e4370fa4bd1e3ff64a0d80e0cee7369d11d73c704e0286fb2865b530acac7a871088fbe92b5edf1000870 + languageName: node + linkType: hard + "@jridgewell/set-array@npm:^1.0.0": version: 1.1.1 resolution: "@jridgewell/set-array@npm:1.1.1" @@ -4713,6 +4909,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/set-array@npm:^1.2.1": + version: 1.2.1 + resolution: "@jridgewell/set-array@npm:1.2.1" + checksum: 832e513a85a588f8ed4f27d1279420d8547743cc37fcad5a5a76fc74bb895b013dfe614d0eed9cb860048e6546b798f8f2652020b4b2ba0561b05caa8c654b10 + languageName: node + linkType: hard + "@jridgewell/source-map@npm:^0.3.2": version: 0.3.2 resolution: "@jridgewell/source-map@npm:0.3.2" @@ -4730,6 +4933,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/sourcemap-codec@npm:^1.4.14": + version: 1.5.0 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" + checksum: 05df4f2538b3b0f998ea4c1cd34574d0feba216fa5d4ccaef0187d12abf82eafe6021cec8b49f9bb4d90f2ba4582ccc581e72986a5fcf4176ae0cfeb04cf52ec + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:^0.3.0": version: 0.3.4 resolution: "@jridgewell/trace-mapping@npm:0.3.4" @@ -4740,6 +4950,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.25": + version: 0.3.25 + resolution: "@jridgewell/trace-mapping@npm:0.3.25" + dependencies: + "@jridgewell/resolve-uri": ^3.1.0 + "@jridgewell/sourcemap-codec": ^1.4.14 + checksum: 9d3c40d225e139987b50c48988f8717a54a8c994d8a948ee42e1412e08988761d0754d7d10b803061cc3aebf35f92a5dbbab493bd0e1a9ef9e89a2130e83ba34 + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:^0.3.7, @jridgewell/trace-mapping@npm:^0.3.9": version: 0.3.13 resolution: "@jridgewell/trace-mapping@npm:0.3.13" @@ -5669,6 +5889,13 @@ __metadata: languageName: node linkType: hard +"@types/trusted-types@npm:^2.0.7": + version: 2.0.7 + resolution: "@types/trusted-types@npm:2.0.7" + checksum: 8e4202766a65877efcf5d5a41b7dd458480b36195e580a3b1085ad21e948bc417d55d6f8af1fd2a7ad008015d4117d5fdfe432731157da3c68678487174e4ba3 + languageName: node + linkType: hard + "@types/unist@npm:*, @types/unist@npm:^2.0.0, @types/unist@npm:^2.0.2": version: 2.0.6 resolution: "@types/unist@npm:2.0.6" @@ -6932,7 +7159,7 @@ __metadata: languageName: node linkType: hard -"async@npm:^2.4.1, async@npm:^2.6.2": +"async@npm:^2.4.1": version: 2.6.3 resolution: "async@npm:2.6.3" dependencies: @@ -6941,6 +7168,15 @@ __metadata: languageName: node linkType: hard +"async@npm:^2.6.4": + version: 2.6.4 + resolution: "async@npm:2.6.4" + dependencies: + lodash: ^4.17.14 + checksum: a52083fb32e1ebe1d63e5c5624038bb30be68ff07a6c8d7dfe35e47c93fc144bd8652cbec869e0ac07d57dde387aa5f1386be3559cdee799cb1f789678d88e19 + languageName: node + linkType: hard + "async@npm:~0.2.9": version: 0.2.10 resolution: "async@npm:0.2.10" @@ -8103,23 +8339,23 @@ __metadata: languageName: node linkType: hard -"body-parser@npm:1.20.0": - version: 1.20.0 - resolution: "body-parser@npm:1.20.0" +"body-parser@npm:1.20.3": + version: 1.20.3 + resolution: "body-parser@npm:1.20.3" dependencies: bytes: 3.1.2 - content-type: ~1.0.4 + content-type: ~1.0.5 debug: 2.6.9 depd: 2.0.0 destroy: 1.2.0 http-errors: 2.0.0 iconv-lite: 0.4.24 on-finished: 2.4.1 - qs: 6.10.3 - raw-body: 2.5.1 + qs: 6.13.0 + raw-body: 2.5.2 type-is: ~1.6.18 unpipe: 1.0.0 - checksum: 12fffdeac82fe20dddcab7074215d5156e7d02a69ae90cbe9fee1ca3efa2f28ef52097cbea76685ee0a1509c71d85abd0056a08e612c09077cad6277a644cf88 + checksum: 1a35c59a6be8d852b00946330141c4f142c6af0f970faa87f10ad74f1ee7118078056706a05ae3093c54dabca9cd3770fa62a170a85801da1a4324f04381167d languageName: node linkType: hard @@ -9264,6 +9500,20 @@ __metadata: languageName: node linkType: hard +"browserslist@npm:^4.24.0": + version: 4.24.2 + resolution: "browserslist@npm:4.24.2" + dependencies: + caniuse-lite: ^1.0.30001669 + electron-to-chromium: ^1.5.41 + node-releases: ^2.0.18 + update-browserslist-db: ^1.1.1 + bin: + browserslist: cli.js + checksum: cf64085f12132d38638f38937a255edb82c7551b164a98577b055dd79719187a816112f7b97b9739e400c4954cd66479c0d7a843cb816e346f4795dc24fd5d97 + languageName: node + linkType: hard + "bser@npm:2.1.1": version: 2.1.1 resolution: "bser@npm:2.1.1" @@ -9505,10 +9755,10 @@ __metadata: languageName: node linkType: hard -"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30000792, caniuse-lite@npm:^1.0.30000805, caniuse-lite@npm:^1.0.30000844, caniuse-lite@npm:^1.0.30001219, caniuse-lite@npm:^1.0.30001264, caniuse-lite@npm:^1.0.30001280, caniuse-lite@npm:^1.0.30001304, caniuse-lite@npm:^1.0.30001317, caniuse-lite@npm:^1.0.30001349": - version: 1.0.30001487 - resolution: "caniuse-lite@npm:1.0.30001487" - checksum: b5a9e72ec165765fb3e07913cc389685ce8a30ac48967f99baec773a4353d2037fb534241e87b3c95d40a5081079be2263710b784883183bb2998b73f7202233 +"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30000792, caniuse-lite@npm:^1.0.30000805, caniuse-lite@npm:^1.0.30000844, caniuse-lite@npm:^1.0.30001219, caniuse-lite@npm:^1.0.30001264, caniuse-lite@npm:^1.0.30001280, caniuse-lite@npm:^1.0.30001304, caniuse-lite@npm:^1.0.30001317, caniuse-lite@npm:^1.0.30001349, caniuse-lite@npm:^1.0.30001669": + version: 1.0.30001687 + resolution: "caniuse-lite@npm:1.0.30001687" + checksum: 20fea782da99d7ff964a9f0573c9eb02762eee2783522f5db5c0a20ba9d9d1cf294aae4162b5ef2f47f729916e6bd0ba457118c6d086c1132d19a46d2d1c61e7 languageName: node linkType: hard @@ -10260,6 +10510,13 @@ __metadata: languageName: node linkType: hard +"content-type@npm:~1.0.5": + version: 1.0.5 + resolution: "content-type@npm:1.0.5" + checksum: 566271e0a251642254cde0f845f9dd4f9856e52d988f4eb0d0dcffbb7a1f8ec98de7a5215fc628f3bce30fe2fb6fd2bc064b562d721658c59b544e2d34ea2766 + languageName: node + linkType: hard + "continuable-cache@npm:^0.3.1": version: 0.3.1 resolution: "continuable-cache@npm:0.3.1" @@ -10276,6 +10533,13 @@ __metadata: languageName: node linkType: hard +"convert-source-map@npm:^2.0.0": + version: 2.0.0 + resolution: "convert-source-map@npm:2.0.0" + checksum: 63ae9933be5a2b8d4509daca5124e20c14d023c820258e484e32dc324d34c2754e71297c94a05784064ad27615037ef677e3f0c00469fb55f409d2bb21261035 + languageName: node + linkType: hard + "cookie-signature@npm:1.0.6": version: 1.0.6 resolution: "cookie-signature@npm:1.0.6" @@ -10290,17 +10554,17 @@ __metadata: languageName: node linkType: hard -"cookie@npm:0.5.0": - version: 0.5.0 - resolution: "cookie@npm:0.5.0" - checksum: 1f4bd2ca5765f8c9689a7e8954183f5332139eb72b6ff783d8947032ec1fdf43109852c178e21a953a30c0dd42257828185be01b49d1eb1a67fd054ca588a180 +"cookie@npm:0.7.1": + version: 0.7.1 + resolution: "cookie@npm:0.7.1" + checksum: cec5e425549b3650eb5c3498a9ba3cde0b9cd419e3b36e4b92739d30b4d89e0b678b98c1ddc209ce7cf958cd3215671fd6ac47aec21f10c2a0cc68abd399d8a7 languageName: node linkType: hard -"cookie@npm:~0.4.1": - version: 0.4.1 - resolution: "cookie@npm:0.4.1" - checksum: bd7c47f5d94ab70ccdfe8210cde7d725880d2fcda06d8e375afbdd82de0c8d3b73541996e9ce57d35f67f672c4ee6d60208adec06b3c5fc94cebb85196084cf8 +"cookie@npm:~0.7.2": + version: 0.7.2 + resolution: "cookie@npm:0.7.2" + checksum: 9bf8555e33530affd571ea37b615ccad9b9a34febbf2c950c86787088eb00a8973690833b0f8ebd6b69b753c62669ea60cec89178c1fb007bf0749abed74f93e languageName: node linkType: hard @@ -11117,7 +11381,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:^3.0.1, debug@npm:^3.1.0, debug@npm:^3.1.1": +"debug@npm:^3.0.1, debug@npm:^3.1.0, debug@npm:^3.2.7": version: 3.2.7 resolution: "debug@npm:3.2.7" dependencies: @@ -11150,6 +11414,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:~4.3.4": + version: 4.3.7 + resolution: "debug@npm:4.3.7" + dependencies: + ms: ^2.1.3 + peerDependenciesMeta: + supports-color: + optional: true + checksum: 822d74e209cd910ef0802d261b150314bbcf36c582ccdbb3e70f0894823c17e49a50d3e66d96b633524263975ca16b6a833f3e3b7e030c157169a5fabac63160 + languageName: node + linkType: hard + "decache@npm:^4.5.1": version: 4.6.0 resolution: "decache@npm:4.6.0" @@ -11358,9 +11634,9 @@ __metadata: linkType: hard "diff@npm:^5.0.0": - version: 5.0.0 - resolution: "diff@npm:5.0.0" - checksum: f19fe29284b633afdb2725c2a8bb7d25761ea54d321d8e67987ac851c5294be4afeab532bd84531e02583a3fe7f4014aa314a3eda84f5590e7a9e6b371ef3b46 + version: 5.2.0 + resolution: "diff@npm:5.2.0" + checksum: 12b63ca9c36c72bafa3effa77121f0581b4015df18bc16bac1f8e263597735649f1a173c26f7eba17fb4162b073fee61788abe49610e6c70a2641fe1895443fd languageName: node linkType: hard @@ -11497,9 +11773,14 @@ __metadata: linkType: hard "dompurify@npm:^3.0.2": - version: 3.0.2 - resolution: "dompurify@npm:3.0.2" - checksum: 768c3bb2f139ce1e1a53faf81aae6abe03e24d0b043e2fda0b70e91ef15bb1df854a35e82d8c519adfe5b81c24bfdad3e0b1085534bfbf427137cb742406c47f + version: 3.2.2 + resolution: "dompurify@npm:3.2.2" + dependencies: + "@types/trusted-types": ^2.0.7 + dependenciesMeta: + "@types/trusted-types": + optional: true + checksum: e4831baa447cc7ed4350ede29f7ec4d2614a59287b6916f3691d287dd4a1c45eb3ce9cb26058edf37b3f2648bbf0a3ca5fb3b74c2f78bdcf6ebb7716c2f14252 languageName: node linkType: hard @@ -11600,10 +11881,10 @@ __metadata: languageName: node linkType: hard -"electron-to-chromium@npm:^1.3.30": - version: 1.3.725 - resolution: "electron-to-chromium@npm:1.3.725" - checksum: 90801f5cc30f76495412ce86cef141cd22924ec75c88c276eaa42435d09370d9df5d8127060dae4fdb96a3ea0b0a726ee228788affb997e7f4172f4e54001c5d +"electron-to-chromium@npm:^1.3.30, electron-to-chromium@npm:^1.5.41": + version: 1.5.71 + resolution: "electron-to-chromium@npm:1.5.71" + checksum: fa86e53aa78f5f11efd922c44eccc3110a08e022e08129361af0e0534e40916f53512e86da51c39b73e4342b22e33862e0bc0568b1f95e325b03e66626c0a15f languageName: node linkType: hard @@ -13533,6 +13814,13 @@ __metadata: languageName: node linkType: hard +"encodeurl@npm:~2.0.0": + version: 2.0.0 + resolution: "encodeurl@npm:2.0.0" + checksum: abf5cd51b78082cf8af7be6785813c33b6df2068ce5191a40ca8b1afe6a86f9230af9a9ce694a5ce4665955e5c1120871826df9c128a642e09c58d592e2807fe + languageName: node + linkType: hard + "encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" @@ -13551,28 +13839,28 @@ __metadata: languageName: node linkType: hard -"engine.io-parser@npm:~5.0.3": - version: 5.0.4 - resolution: "engine.io-parser@npm:5.0.4" - checksum: d4ad0cef6ff63c350e35696da9bb3dbd180f67b56e93e90375010cc40393e6c0639b780d5680807e1d93a7e2e3d7b4a1c3b27cf75db28eb8cbf605bc1497da03 +"engine.io-parser@npm:~5.2.1": + version: 5.2.3 + resolution: "engine.io-parser@npm:5.2.3" + checksum: a76d998b794ce8bbcade833064d949715781fdb9e9cf9b33ecf617d16355ddfd7772f12bb63aaec0f497d63266c6db441129c5aa24c60582270f810c696a6cf8 languageName: node linkType: hard -"engine.io@npm:~6.2.0": - version: 6.2.0 - resolution: "engine.io@npm:6.2.0" +"engine.io@npm:~6.6.0": + version: 6.6.2 + resolution: "engine.io@npm:6.6.2" dependencies: "@types/cookie": ^0.4.1 "@types/cors": ^2.8.12 "@types/node": ">=10.0.0" accepts: ~1.3.4 base64id: 2.0.0 - cookie: ~0.4.1 + cookie: ~0.7.2 cors: ~2.8.5 debug: ~4.3.1 - engine.io-parser: ~5.0.3 - ws: ~8.2.3 - checksum: cc485c5ba2e0c4f6ca02dcafd192b22f9dad89d01dc815005298780d3fb910db4cebab4696e8615290c473c2eeb259e8bee2a1fb7ab594d9c80f9f3485771911 + engine.io-parser: ~5.2.1 + ws: ~8.17.1 + checksum: c474feff30fe8c816cccf1642b2f4980cacbff51afcda53c522cbeec4d0ed4047dfbcbeaff694bd88a5de51b3df832fbfb58293bbbf8ddba85459cb45be5f9da languageName: node linkType: hard @@ -13791,6 +14079,13 @@ __metadata: languageName: node linkType: hard +"escalade@npm:^3.2.0": + version: 3.2.0 + resolution: "escalade@npm:3.2.0" + checksum: 47b029c83de01b0d17ad99ed766347b974b0d628e848de404018f3abee728e987da0d2d370ad4574aa3d5b5bfc368754fd085d69a30f8e75903486ec4b5b709e + languageName: node + linkType: hard + "escape-html@npm:~1.0.3": version: 1.0.3 resolution: "escape-html@npm:1.0.3" @@ -14399,41 +14694,41 @@ __metadata: linkType: hard "express@npm:^4.17.2": - version: 4.18.1 - resolution: "express@npm:4.18.1" + version: 4.21.2 + resolution: "express@npm:4.21.2" dependencies: accepts: ~1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.0 + body-parser: 1.20.3 content-disposition: 0.5.4 content-type: ~1.0.4 - cookie: 0.5.0 + cookie: 0.7.1 cookie-signature: 1.0.6 debug: 2.6.9 depd: 2.0.0 - encodeurl: ~1.0.2 + encodeurl: ~2.0.0 escape-html: ~1.0.3 etag: ~1.8.1 - finalhandler: 1.2.0 + finalhandler: 1.3.1 fresh: 0.5.2 http-errors: 2.0.0 - merge-descriptors: 1.0.1 + merge-descriptors: 1.0.3 methods: ~1.1.2 on-finished: 2.4.1 parseurl: ~1.3.3 - path-to-regexp: 0.1.7 + path-to-regexp: 0.1.12 proxy-addr: ~2.0.7 - qs: 6.10.3 + qs: 6.13.0 range-parser: ~1.2.1 safe-buffer: 5.2.1 - send: 0.18.0 - serve-static: 1.15.0 + send: 0.19.0 + serve-static: 1.16.2 setprototypeof: 1.2.0 statuses: 2.0.1 type-is: ~1.6.18 utils-merge: 1.0.1 vary: ~1.1.2 - checksum: c3d44c92e48226ef32ec978becfedb0ecf0ca21316bfd33674b3c5d20459840584f2325726a4f17f33d9c99f769636f728982d1c5433a5b6fe6eb95b8cf0c854 + checksum: 3aef1d355622732e20b8f3a7c112d4391d44e2131f4f449e1f273a309752a41abfad714e881f177645517cbe29b3ccdc10b35e7e25c13506114244a5b72f549d languageName: node linkType: hard @@ -14754,22 +15049,22 @@ __metadata: languageName: node linkType: hard -"finalhandler@npm:1.2.0": - version: 1.2.0 - resolution: "finalhandler@npm:1.2.0" +"finalhandler@npm:1.3.1": + version: 1.3.1 + resolution: "finalhandler@npm:1.3.1" dependencies: debug: 2.6.9 - encodeurl: ~1.0.2 + encodeurl: ~2.0.0 escape-html: ~1.0.3 on-finished: 2.4.1 parseurl: ~1.3.3 statuses: 2.0.1 unpipe: ~1.0.0 - checksum: 92effbfd32e22a7dff2994acedbd9bcc3aa646a3e919ea6a53238090e87097f8ef07cced90aa2cc421abdf993aefbdd5b00104d55c7c5479a8d00ed105b45716 + checksum: a8c58cd97c9cd47679a870f6833a7b417043f5a288cd6af6d0f49b476c874a506100303a128b6d3b654c3d74fa4ff2ffed68a48a27e8630cda5c918f2977dcf4 languageName: node linkType: hard -"find-babel-config@npm:^1.1.0, find-babel-config@npm:^1.2.0": +"find-babel-config@npm:^1.1.0": version: 1.2.0 resolution: "find-babel-config@npm:1.2.0" dependencies: @@ -14779,6 +15074,16 @@ __metadata: languageName: node linkType: hard +"find-babel-config@npm:^1.2.0": + version: 1.2.2 + resolution: "find-babel-config@npm:1.2.2" + dependencies: + json5: ^1.0.2 + path-exists: ^3.0.0 + checksum: 9dd8ef0f47c5d83f6bf4106c1e21c6e62dd8b11d32ce0df3700b141ca119c63bd849cc3ade7e54c39c8a235b9c2a6ac598acda801f82582514b7b9c64027771d + languageName: node + linkType: hard + "find-cache-dir@npm:^2.1.0": version: 2.1.0 resolution: "find-cache-dir@npm:2.1.0" @@ -14899,16 +15204,16 @@ __metadata: languageName: node linkType: hard -"fireworm@npm:^0.7.0": - version: 0.7.1 - resolution: "fireworm@npm:0.7.1" +"fireworm@npm:^0.7.2": + version: 0.7.2 + resolution: "fireworm@npm:0.7.2" dependencies: async: ~0.2.9 is-type: 0.0.1 lodash.debounce: ^3.1.1 lodash.flatten: ^3.0.2 minimatch: ^3.0.2 - checksum: 118ac822c5594f832db082475ac40e3d5532a84ed1aa74c04aee6827e3940b62cde5551308643afdead594f07aa5fb24a140eec51d4fa63945729d4c0fa6c401 + checksum: 0ad72e2ff34df45a5264da39e504b47964e350fbe575186306c8743799a0330c227870efd67e65d031532b14b5b01a9602c5c691c818b7902818bfd44a8b1aa8 languageName: node linkType: hard @@ -15748,7 +16053,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.3": +"graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.15": version: 4.2.6 resolution: "graceful-fs@npm:4.2.6" checksum: 792e64aafda05a151289f83eaa16aff34ef259658cefd65393883d959409f5a2389b0ec9ebf28f3d21f1b0ddc8f594a1162ae9b18e2b507a6799a70706ec573d @@ -15762,7 +16067,7 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:^4.2.6": +"graceful-fs@npm:^4.1.3, graceful-fs@npm:^4.2.6": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" checksum: ac85f94da92d8eb6b7f5a8b20ce65e43d66761c55ce85ac96df6865308390da45a8d3f0296dd3a663de65d30ba497bd46c696cc1e248c72b13d6d567138a4fc7 @@ -16068,11 +16373,11 @@ __metadata: linkType: hard "hosted-git-info@npm:^4.0.1": - version: 4.0.2 - resolution: "hosted-git-info@npm:4.0.2" + version: 4.1.0 + resolution: "hosted-git-info@npm:4.1.0" dependencies: lru-cache: ^6.0.0 - checksum: d1b2d7720398ce96a788bd38d198fbddce089a2381f63cfb01743e6c7e5aed656e5547fe74090fb9fe53b2cb785b0e8c9ebdddadff48ed26bb471dd23cd25458 + checksum: c3f87b3c2f7eb8c2748c8f49c0c2517c9a95f35d26f4bf54b2a8cba05d2e668f3753548b6ea366b18ec8dadb4e12066e19fa382a01496b0ffa0497eb23cbe461 languageName: node linkType: hard @@ -16368,20 +16673,20 @@ __metadata: languageName: node linkType: hard -"inflection@npm:^1.13.1, inflection@npm:^1.13.2": - version: 1.13.2 - resolution: "inflection@npm:1.13.2" - checksum: e7ad0559384ed7c526813404bde843f8f17941d47625ad60fc3b09e46efde873dd9840818007c6bd4dbe388e6248fa033d5a8c405c5fc62738c51b118a0e940f - languageName: node - linkType: hard - -"inflection@npm:~1.13.2": +"inflection@npm:^1.13.1, inflection@npm:~1.13.2": version: 1.13.4 resolution: "inflection@npm:1.13.4" checksum: 6744feede9998ad8abd2b1db4af79f494a166e656a0aa949d90c8f4a945c1d07038a3756bf7af78c8f6fce368ba213a7ebf35da3edeffd39f1da0ff465eed6eb languageName: node linkType: hard +"inflection@npm:^1.13.2": + version: 1.13.2 + resolution: "inflection@npm:1.13.2" + checksum: e7ad0559384ed7c526813404bde843f8f17941d47625ad60fc3b09e46efde873dd9840818007c6bd4dbe388e6248fa033d5a8c405c5fc62738c51b118a0e940f + languageName: node + linkType: hard + "inflight@npm:^1.0.4": version: 1.0.6 resolution: "inflight@npm:1.0.6" @@ -17197,6 +17502,15 @@ __metadata: languageName: node linkType: hard +"jsesc@npm:^3.0.2": + version: 3.0.2 + resolution: "jsesc@npm:3.0.2" + bin: + jsesc: bin/jsesc + checksum: a36d3ca40574a974d9c2063bf68c2b6141c20da8f2a36bd3279fc802563f35f0527a6c828801295bdfb2803952cf2cf387786c2c90ed564f88d5782475abfe3c + languageName: node + linkType: hard + "jsesc@npm:~0.3.x": version: 0.3.0 resolution: "jsesc@npm:0.3.0" @@ -17293,6 +17607,17 @@ __metadata: languageName: node linkType: hard +"json5@npm:^1.0.2": + version: 1.0.2 + resolution: "json5@npm:1.0.2" + dependencies: + minimist: ^1.2.0 + bin: + json5: lib/cli.js + checksum: 866458a8c58a95a49bef3adba929c625e82532bcff1fe93f01d29cb02cac7c3fe1f4b79951b7792c2da9de0b32871a8401a6e3c5b36778ad852bf5b8a61165d7 + languageName: node + linkType: hard + "json5@npm:^2.1.2": version: 2.2.0 resolution: "json5@npm:2.2.0" @@ -17313,6 +17638,15 @@ __metadata: languageName: node linkType: hard +"json5@npm:^2.2.3": + version: 2.2.3 + resolution: "json5@npm:2.2.3" + bin: + json5: lib/cli.js + checksum: 2a7436a93393830bce797d4626275152e37e877b265e94ca69c99e3d20c2b9dab021279146a39cdb700e71b2dd32a4cebd1514cd57cee102b1af906ce5040349 + languageName: node + linkType: hard + "jsondiffpatch@npm:^0.4.1": version: 0.4.1 resolution: "jsondiffpatch@npm:0.4.1" @@ -17505,11 +17839,11 @@ __metadata: linkType: hard "linkify-it@npm:^3.0.1": - version: 3.0.2 - resolution: "linkify-it@npm:3.0.2" + version: 3.0.3 + resolution: "linkify-it@npm:3.0.3" dependencies: uc.micro: ^1.0.1 - checksum: 08e14854ec3c29e3578311b1cd95e469952a27f191633189a23890628939fc45c6d84fa4495abb9f7f06e60f73a31b8881d834214864d46db914a09ffc7889ae + checksum: 31367a4bb70c5bbc9703246236b504b0a8e049bcd4e0de4291fa50f0ebdebf235b5eb54db6493cb0b1319357c6eeafc4324c9f4aa34b0b943d9f2e11a1268fbc languageName: node linkType: hard @@ -17764,13 +18098,6 @@ __metadata: languageName: node linkType: hard -"lodash.assignin@npm:^4.1.0": - version: 4.2.0 - resolution: "lodash.assignin@npm:4.2.0" - checksum: 4b55bc1d65ccd7648fdba8a4316d10546929bf0beb5950830d86c559948cf170f0e65b77c95e66b45b511b85a31161714de8b2008d2537627ef3c7759afe36a6 - languageName: node - linkType: hard - "lodash.camelcase@npm:^4.3.0": version: 4.3.0 resolution: "lodash.camelcase@npm:4.3.0" @@ -17778,14 +18105,7 @@ __metadata: languageName: node linkType: hard -"lodash.castarray@npm:^4.4.0": - version: 4.4.0 - resolution: "lodash.castarray@npm:4.4.0" - checksum: fca8c7047e0ae2738b0b2503fb00157ae0ff6d8a1b716f87ed715b22560e09de438c75b65e01a7e44ceb41c5b31dce2eb576e46db04beb9c699c498e03cbd00f - languageName: node - linkType: hard - -"lodash.clonedeep@npm:^4.4.1, lodash.clonedeep@npm:^4.5.0": +"lodash.clonedeep@npm:^4.5.0": version: 4.5.0 resolution: "lodash.clonedeep@npm:4.5.0" checksum: 92c46f094b064e876a23c97f57f81fbffd5d760bf2d8a1c61d85db6d1e488c66b0384c943abee4f6af7debf5ad4e4282e74ff83177c9e63d8ff081a4837c3489 @@ -17822,7 +18142,7 @@ __metadata: languageName: node linkType: hard -"lodash.find@npm:^4.5.1, lodash.find@npm:^4.6.0": +"lodash.find@npm:^4.6.0": version: 4.6.0 resolution: "lodash.find@npm:4.6.0" checksum: b737f849a4fe36f5c3664ea636780dda2fde18335021faf80cdfdcb300ed75441da6f55cfd6de119092d8bb2ddbc4433f4a8de4b99c0b9c8640465b0901c717c @@ -18557,6 +18877,13 @@ __metadata: languageName: node linkType: hard +"merge-descriptors@npm:1.0.3": + version: 1.0.3 + resolution: "merge-descriptors@npm:1.0.3" + checksum: 52117adbe0313d5defa771c9993fe081e2d2df9b840597e966aadafde04ae8d0e3da46bac7ca4efc37d4d2b839436582659cd49c6a43eacb3fe3050896a105d1 + languageName: node + linkType: hard + "merge-stream@npm:^2.0.0": version: 2.0.0 resolution: "merge-stream@npm:2.0.0" @@ -19026,6 +19353,17 @@ __metadata: languageName: node linkType: hard +"mkdirp@npm:^0.5.6": + version: 0.5.6 + resolution: "mkdirp@npm:0.5.6" + dependencies: + minimist: ^1.2.6 + bin: + mkdirp: bin/cmd.js + checksum: 0c91b721bb12c3f9af4b77ebf73604baf350e64d80df91754dc509491ae93bf238581e59c7188360cec7cb62fc4100959245a42cfe01834efedc5e9d068376c2 + languageName: node + linkType: hard + "mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4": version: 1.0.4 resolution: "mkdirp@npm:1.0.4" @@ -19035,6 +19373,15 @@ __metadata: languageName: node linkType: hard +"mkdirp@npm:^3.0.1": + version: 3.0.1 + resolution: "mkdirp@npm:3.0.1" + bin: + mkdirp: dist/cjs/src/bin.js + checksum: 972deb188e8fb55547f1e58d66bd6b4a3623bf0c7137802582602d73e6480c1c2268dcbafbfb1be466e00cc7e56ac514d7fd9334b7cf33e3e2ab547c16f83a8d + languageName: node + linkType: hard + "mktemp@npm:~0.4.0": version: 0.4.0 resolution: "mktemp@npm:0.4.0" @@ -19063,9 +19410,9 @@ __metadata: linkType: hard "mout@npm:^1.0.0": - version: 1.2.2 - resolution: "mout@npm:1.2.2" - checksum: e6c4249e9a5e603af8b4ae7172572e266f7cbd2edbf27824e2e06dd288adc66e67170f4a061ecc388429046aa93a7e3946e35139fd0767f76dd10d075fbca74f + version: 1.2.4 + resolution: "mout@npm:1.2.4" + checksum: 761801ab6616cea1a7f3c2082d1e2fb0b984898cc92117c70f34aab104723285eadf03545b5e092d44a17d6cfbd367a005064d7b23a8517ee4f6b77a8d345561 languageName: node linkType: hard @@ -19115,7 +19462,7 @@ __metadata: languageName: node linkType: hard -"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1": +"ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" checksum: aa92de608021b242401676e35cfa5aa42dd70cbdc082b916da7fb925c542173e36bce97ea3e804923fe92c0ad991434e4a38327e15a1b5b5f945d66df615ae6d @@ -19248,11 +19595,16 @@ __metadata: linkType: hard "node-fetch@npm:^2.6.0": - version: 2.6.6 - resolution: "node-fetch@npm:2.6.6" + version: 2.7.0 + resolution: "node-fetch@npm:2.7.0" dependencies: whatwg-url: ^5.0.0 - checksum: ee8290626bdb73629c59722b75dcf4b9b6a67c1ed7eb9102e368479c4a13b56a48c2bb3ad71571e378e98c8b2c64c820e11f9cd39e4b8557dd138ad571ef9a42 + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + checksum: d76d2f5edb451a3f05b15115ec89fc6be39de37c6089f1b6368df03b91e1633fd379a7e01b7ab05089a25034b2023d959b47e59759cb38d88341b2459e89d6e5 languageName: node linkType: hard @@ -19356,6 +19708,13 @@ __metadata: languageName: node linkType: hard +"node-releases@npm:^2.0.18": + version: 2.0.18 + resolution: "node-releases@npm:2.0.18" + checksum: ef55a3d853e1269a6d6279b7692cd6ff3e40bc74947945101138745bfdc9a5edabfe72cb19a31a8e45752e1910c4c65c77d931866af6357f242b172b7283f5b3 + languageName: node + linkType: hard + "node-releases@npm:^2.0.2": version: 2.0.2 resolution: "node-releases@npm:2.0.2" @@ -20157,6 +20516,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:0.1.12": + version: 0.1.12 + resolution: "path-to-regexp@npm:0.1.12" + checksum: ab237858bee7b25ecd885189f175ab5b5161e7b712b360d44f5c4516b8d271da3e4bf7bf0a7b9153ecb04c7d90ce8ff5158614e1208819cf62bac2b08452722e + languageName: node + linkType: hard + "path-to-regexp@npm:0.1.7": version: 0.1.7 resolution: "path-to-regexp@npm:0.1.7" @@ -20223,6 +20589,13 @@ __metadata: languageName: node linkType: hard +"picocolors@npm:^1.1.0": + version: 1.1.1 + resolution: "picocolors@npm:1.1.1" + checksum: e1cf46bf84886c79055fdfa9dcb3e4711ad259949e3565154b004b260cd356c5d54b31a1437ce9782624bf766272fe6b0154f5f0c744fb7af5d454d2b60db045 + languageName: node + linkType: hard + "picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3": version: 2.2.3 resolution: "picomatch@npm:2.2.3" @@ -20333,13 +20706,13 @@ __metadata: linkType: hard "portfinder@npm:^1.0.28": - version: 1.0.28 - resolution: "portfinder@npm:1.0.28" + version: 1.0.32 + resolution: "portfinder@npm:1.0.32" dependencies: - async: ^2.6.2 - debug: ^3.1.1 - mkdirp: ^0.5.5 - checksum: 91fef602f13f8f4c64385d0ad2a36cc9dc6be0b8d10a2628ee2c3c7b9917ab4fefb458815b82cea2abf4b785cd11c9b4e2d917ac6fa06f14b6fa880ca8f8928c + async: ^2.6.4 + debug: ^3.2.7 + mkdirp: ^0.5.6 + checksum: 116b4aed1b9e16f6d5503823d966d9ffd41b1c2339e27f54c06cd2f3015a9d8ef53e2a53b57bc0a25af0885977b692007353aa28f9a0a98a44335cb50487240d languageName: node linkType: hard @@ -20884,15 +21257,15 @@ __metadata: languageName: node linkType: hard -"raw-body@npm:2.5.1": - version: 2.5.1 - resolution: "raw-body@npm:2.5.1" +"raw-body@npm:2.5.2": + version: 2.5.2 + resolution: "raw-body@npm:2.5.2" dependencies: bytes: 3.1.2 http-errors: 2.0.0 iconv-lite: 0.4.24 unpipe: 1.0.0 - checksum: 5362adff1575d691bb3f75998803a0ffed8c64eabeaa06e54b4ada25a0cd1b2ae7f4f5ec46565d1bec337e08b5ac90c76eaa0758de6f72a633f025d754dec29e + checksum: ba1583c8d8a48e8fbb7a873fdbb2df66ea4ff83775421bfe21ee120140949ab048200668c47d9ae3880012f6e217052690628cf679ddfbd82c9fc9358d574676 languageName: node linkType: hard @@ -21397,9 +21770,9 @@ __metadata: linkType: hard "reselect@npm:^4.0.0": - version: 4.0.0 - resolution: "reselect@npm:4.0.0" - checksum: ac7dfc9ef2cdb42b6fc87a856f3ce904c2e4363a2bc1e6fb7eea5f78902a6f506e4388e6509752984877c6dbfe501100c076671d334799eb5a1bfe9936cb2c12 + version: 4.1.8 + resolution: "reselect@npm:4.1.8" + checksum: a4ac87cedab198769a29be92bc221c32da76cfdad6911eda67b4d3e7136dca86208c3b210e31632eae31ebd2cded18596f0dd230d3ccc9e978df22f233b5583e languageName: node linkType: hard @@ -21786,9 +22159,9 @@ __metadata: linkType: hard "safe-stable-stringify@npm:^2.3.1": - version: 2.3.1 - resolution: "safe-stable-stringify@npm:2.3.1" - checksum: a0a0bad0294c3e2a9d1bf3cf2b1096dfb83c162d09a5e4891e488cce082120bd69161d2a92aae7fc48255290f17700decae9c89a07fe139794e61b5c8b411377 + version: 2.5.0 + resolution: "safe-stable-stringify@npm:2.5.0" + checksum: d3ce103ed43c6c2f523e39607208bfb1c73aa48179fc5be53c3aa97c118390bffd4d55e012f5393b982b65eb3e0ee954dd57b547930d3f242b0053dcdb923d17 languageName: node linkType: hard @@ -21837,10 +22210,10 @@ __metadata: languageName: node linkType: hard -"sass-svg-uri@npm:^1.0.0": - version: 1.0.0 - resolution: "sass-svg-uri@npm:1.0.0" - checksum: 75a2af6cc4ac44c262309f51c290a1a5120ab5cd45cabafd0faabd9ad3bb6898417d3993fdfdf65efbf0ff9878d105d2bb8e1502db29f13c41974fb4adaa3f1a +"sass-svg-uri@npm:^2.0.0": + version: 2.0.0 + resolution: "sass-svg-uri@npm:2.0.0" + checksum: 5357875b889310e13a78007ceec8ac5bdd28ac48bbba33f3e912e0dfee2f28e065229e4e0c6da9774f9f3c1f35b0fa8d1a6c44ab638b5c7c2b46c8ae2d846430 languageName: node linkType: hard @@ -21970,6 +22343,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^6.3.1": + version: 6.3.1 + resolution: "semver@npm:6.3.1" + bin: + semver: bin/semver.js + checksum: ae47d06de28836adb9d3e25f22a92943477371292d9b665fb023fae278d345d508ca1958232af086d85e0155aee22e313e100971898bbb8d5d89b8b1d4054ca2 + languageName: node + linkType: hard + "semver@npm:^7.3.7": version: 7.3.7 resolution: "semver@npm:7.3.7" @@ -22013,9 +22395,9 @@ __metadata: languageName: node linkType: hard -"send@npm:0.18.0": - version: 0.18.0 - resolution: "send@npm:0.18.0" +"send@npm:0.19.0": + version: 0.19.0 + resolution: "send@npm:0.19.0" dependencies: debug: 2.6.9 depd: 2.0.0 @@ -22030,7 +22412,7 @@ __metadata: on-finished: 2.4.1 range-parser: ~1.2.1 statuses: 2.0.1 - checksum: 74fc07ebb58566b87b078ec63e5a3e41ecd987e4272ba67b7467e86c6ad51bc6b0b0154133b6d8b08a2ddda360464f71382f7ef864700f34844a76c8027817a8 + checksum: 5ae11bd900c1c2575525e2aa622e856804e2f96a09281ec1e39610d089f53aa69e13fd8db84b52f001d0318cf4bb0b3b904ad532fc4c0014eb90d32db0cff55f languageName: node linkType: hard @@ -22055,15 +22437,15 @@ __metadata: languageName: node linkType: hard -"serve-static@npm:1.15.0": - version: 1.15.0 - resolution: "serve-static@npm:1.15.0" +"serve-static@npm:1.16.2": + version: 1.16.2 + resolution: "serve-static@npm:1.16.2" dependencies: - encodeurl: ~1.0.2 + encodeurl: ~2.0.0 escape-html: ~1.0.3 parseurl: ~1.3.3 - send: 0.18.0 - checksum: af57fc13be40d90a12562e98c0b7855cf6e8bd4c107fe9a45c212bf023058d54a1871b1c89511c3958f70626fff47faeb795f5d83f8cf88514dbaeb2b724464d + send: 0.19.0 + checksum: dffc52feb4cc5c68e66d0c7f3c1824d4e989f71050aefc9bd5f822a42c54c9b814f595fc5f2b717f4c7cc05396145f3e90422af31186a93f76cf15f707019759 languageName: node linkType: hard @@ -22324,34 +22706,38 @@ __metadata: languageName: node linkType: hard -"socket.io-adapter@npm:~2.4.0": - version: 2.4.0 - resolution: "socket.io-adapter@npm:2.4.0" - checksum: a84639946dce13547b95f6e09fe167cdcd5d80941afc2e46790cc23384e0fd3c901e690ecc9bdd600939ce6292261ee15094a0b486f797ed621cfc8783d87a0c +"socket.io-adapter@npm:~2.5.2": + version: 2.5.5 + resolution: "socket.io-adapter@npm:2.5.5" + dependencies: + debug: ~4.3.4 + ws: ~8.17.1 + checksum: fc52253c31d5fec24abc9bcd8d6557545fd1604387c64328def142e9a3d31c92ee8635839d668454fcdc0e7bb0442e8655623879e07b127df12756c28ef7632e languageName: node linkType: hard -"socket.io-parser@npm:~4.2.0": - version: 4.2.1 - resolution: "socket.io-parser@npm:4.2.1" +"socket.io-parser@npm:~4.2.4": + version: 4.2.4 + resolution: "socket.io-parser@npm:4.2.4" dependencies: "@socket.io/component-emitter": ~3.1.0 debug: ~4.3.1 - checksum: 2582202f22538d7e6b4436991378cb4cea3b2f8219cda24923ae35afd291ab5ad6120e7d093e41738256b6c6ad10c667dd25753c2d9a2340fead04e9286f152d + checksum: 61540ef99af33e6a562b9effe0fad769bcb7ec6a301aba5a64b3a8bccb611a0abdbe25f469933ab80072582006a78ca136bf0ad8adff9c77c9953581285e2263 languageName: node linkType: hard -"socket.io@npm:^4.1.2": - version: 4.5.2 - resolution: "socket.io@npm:4.5.2" +"socket.io@npm:^4.5.4": + version: 4.8.1 + resolution: "socket.io@npm:4.8.1" dependencies: accepts: ~1.3.4 base64id: ~2.0.0 + cors: ~2.8.5 debug: ~4.3.2 - engine.io: ~6.2.0 - socket.io-adapter: ~2.4.0 - socket.io-parser: ~4.2.0 - checksum: 8527dd78fa3cf483a2cf0f09f64c4591186931b6765e5d8456dd3022b8786407952e3b931a83a86513c9f56852442e12f3497c761a113113e32b0c867c5ad5a7 + engine.io: ~6.6.0 + socket.io-adapter: ~2.5.2 + socket.io-parser: ~4.2.4 + checksum: d5e4d7eabba7a04c0d130a7b34c57050a1b4694e5b9eb9bd0a40dd07c1d635f3d5cacc15442f6135be8b2ecdad55dad08ee576b5c74864508890ff67329722fa languageName: node linkType: hard @@ -23259,8 +23645,8 @@ __metadata: linkType: hard "testem@npm:^3.6.0": - version: 3.9.0 - resolution: "testem@npm:3.9.0" + version: 3.15.2 + resolution: "testem@npm:3.15.2" dependencies: "@xmldom/xmldom": ^0.8.0 backbone: ^1.1.2 @@ -23271,29 +23657,25 @@ __metadata: consolidate: ^0.16.0 execa: ^1.0.0 express: ^4.10.7 - fireworm: ^0.7.0 + fireworm: ^0.7.2 glob: ^7.0.4 http-proxy: ^1.13.1 js-yaml: ^3.2.5 - lodash.assignin: ^4.1.0 - lodash.castarray: ^4.4.0 - lodash.clonedeep: ^4.4.1 - lodash.find: ^4.5.1 - lodash.uniqby: ^4.7.0 - mkdirp: ^1.0.4 + lodash: ^4.17.21 + mkdirp: ^3.0.1 mustache: ^4.2.0 node-notifier: ^10.0.0 npmlog: ^6.0.0 printf: ^0.6.1 rimraf: ^3.0.2 - socket.io: ^4.1.2 + socket.io: ^4.5.4 spawn-args: ^0.2.0 styled_string: 0.0.1 tap-parser: ^7.0.0 tmp: 0.0.33 bin: testem: testem.js - checksum: c2c0668dfeed4b5327391eee8a8fe92b735acab82d921b8b975dc56220b6c4c1ac9d6a8a4fc15dd9f1cb50d7054885060e3b9b86990453d71f183b83c6b74adb + checksum: 7fec8b3df50907a5d600cd12f23803147e62dbb3370560fe73114e0398bb0ff41c6b863b01da868d2a28c1700d5f7c3fef9ff66d04dd4aed1b30b0ec19c1e096 languageName: node linkType: hard @@ -24024,6 +24406,20 @@ __metadata: languageName: node linkType: hard +"update-browserslist-db@npm:^1.1.1": + version: 1.1.1 + resolution: "update-browserslist-db@npm:1.1.1" + dependencies: + escalade: ^3.2.0 + picocolors: ^1.1.0 + peerDependencies: + browserslist: ">= 4.21.0" + bin: + update-browserslist-db: cli.js + checksum: 2ea11bd2562122162c3e438d83a1f9125238c0844b6d16d366e3276d0c0acac6036822dc7df65fc5a89c699cdf9f174acf439c39bedf3f9a2f3983976e4b4c3e + languageName: node + linkType: hard + "update-section@npm:^0.3.3": version: 0.3.3 resolution: "update-section@npm:0.3.3" @@ -24331,7 +24727,7 @@ __metadata: qunit: ^2.19.1 qunit-dom: ^2.0.0 sass: ^1.58.3 - sass-svg-uri: ^1.0.0 + sass-svg-uri: ^2.0.0 shell-quote: ^1.6.1 string.prototype.endswith: ^0.2.0 string.prototype.startswith: ^0.2.0 @@ -24863,9 +25259,9 @@ __metadata: linkType: hard "workerpool@npm:^6.2.0": - version: 6.2.1 - resolution: "workerpool@npm:6.2.1" - checksum: c2c6eebbc5225f10f758d599a5c016fa04798bcc44e4c1dffb34050cd361d7be2e97891aa44419e7afe647b1f767b1dc0b85a5e046c409d890163f655028b09d + version: 6.5.1 + resolution: "workerpool@npm:6.5.1" + checksum: f86d13f9139c3a57c5a5867e81905cd84134b499849405dec2ffe5b1acd30dabaa1809f6f6ee603a7c65e1e4325f21509db6b8398eaf202c8b8f5809e26a2e16 languageName: node linkType: hard @@ -24948,18 +25344,18 @@ __metadata: languageName: node linkType: hard -"ws@npm:~8.2.3": - version: 8.2.3 - resolution: "ws@npm:8.2.3" +"ws@npm:~8.17.1": + version: 8.17.1 + resolution: "ws@npm:8.17.1" peerDependencies: bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 + utf-8-validate: ">=5.0.2" peerDependenciesMeta: bufferutil: optional: true utf-8-validate: optional: true - checksum: c869296ccb45f218ac6d32f8f614cd85b50a21fd434caf11646008eef92173be53490810c5c23aea31bc527902261fbfd7b062197eea341b26128d4be56a85e4 + checksum: 442badcce1f1178ec87a0b5372ae2e9771e07c4929a3180321901f226127f252441e8689d765aa5cfba5f50ac60dd830954afc5aeae81609aefa11d3ddf5cecf languageName: node linkType: hard diff --git a/vault/acl.go b/vault/acl.go index 3f17d8cada..600dc44d75 100644 --- a/vault/acl.go +++ b/vault/acl.go @@ -302,6 +302,9 @@ func (a *ACL) Capabilities(ctx context.Context, path string) (pathCapabilities [ if capabilities&PatchCapabilityInt > 0 { pathCapabilities = append(pathCapabilities, PatchCapability) } + if capabilities&ScanCapabilityInt > 0 { + pathCapabilities = append(pathCapabilities, ScanCapability) + } // If "deny" is explicitly set or if the path has no capabilities at all, // set the path capabilities to "deny" @@ -427,6 +430,9 @@ CHECK: case logical.PatchOperation: operationAllowed = capabilities&PatchCapabilityInt > 0 grantingPolicies = permissions.GrantingPoliciesMap[PatchCapabilityInt] + case logical.ScanOperation: + operationAllowed = capabilities&ScanCapabilityInt > 0 + grantingPolicies = permissions.GrantingPoliciesMap[ScanCapabilityInt] // These three re-use UpdateCapabilityInt since that's the most appropriate // capability/operation mapping diff --git a/vault/acl_test.go b/vault/acl_test.go index 2b5dd33715..72916f48e1 100644 --- a/vault/acl_test.go +++ b/vault/acl_test.go @@ -247,6 +247,10 @@ func testACLSingle(t *testing.T, ns *namespace.Namespace) { {logical.PatchOperation, "baz/quux", true, false}, {logical.ListOperation, "baz/quux", false, false}, {logical.UpdateOperation, "baz/quux", false, false}, + {logical.ScanOperation, "baz/quux", false, false}, + + {logical.ScanOperation, "asdf/fdsa", true, false}, + {logical.ListOperation, "asdf/fdsa", false, false}, // Path segment wildcards {logical.ReadOperation, "test/foo/bar/segment", false, false}, @@ -1040,6 +1044,9 @@ path "1/2/+" { path "1/2/+/+" { capabilities = ["update"] } +path "asdf/fdsa" { + capabilities = ["scan"] +} ` var aclPolicy2 = ` diff --git a/vault/logical_system.go b/vault/logical_system.go index 24a8874c01..6c3f30b019 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -3920,7 +3920,8 @@ func hasMountAccess(ctx context.Context, acl *ACL, path string) bool { perms.CapabilitiesBitmap&ReadCapabilityInt > 0, perms.CapabilitiesBitmap&SudoCapabilityInt > 0, perms.CapabilitiesBitmap&UpdateCapabilityInt > 0, - perms.CapabilitiesBitmap&PatchCapabilityInt > 0: + perms.CapabilitiesBitmap&PatchCapabilityInt > 0, + perms.CapabilitiesBitmap&ScanCapabilityInt > 0: aclCapabilitiesGiven = true @@ -4255,6 +4256,9 @@ func (b *SystemBackend) pathInternalUIResultantACL(ctx context.Context, req *log if perms.CapabilitiesBitmap&PatchCapabilityInt > 0 { capabilities = append(capabilities, PatchCapability) } + if perms.CapabilitiesBitmap&ScanCapabilityInt > 0 { + capabilities = append(capabilities, ScanCapability) + } // If "deny" is explicitly set or if the path has no capabilities at all, // set the path capabilities to "deny" diff --git a/vault/policy.go b/vault/policy.go index 4c990bb4b6..f4d1724dab 100644 --- a/vault/policy.go +++ b/vault/policy.go @@ -31,6 +31,7 @@ const ( SudoCapability = "sudo" RootCapability = "root" PatchCapability = "patch" + ScanCapability = "scan" // Backwards compatibility OldDenyPathPolicy = "deny" @@ -48,6 +49,7 @@ const ( ListCapabilityInt SudoCapabilityInt PatchCapabilityInt + ScanCapabilityInt ) type PolicyType uint32 @@ -77,6 +79,7 @@ var cap2Int = map[string]uint32{ ListCapability: ListCapabilityInt, SudoCapability: SudoCapabilityInt, PatchCapability: PatchCapabilityInt, + ScanCapability: ScanCapabilityInt, } // Policy is used to represent the policy specified by an ACL configuration. @@ -384,7 +387,7 @@ func parsePaths(result *Policy, list *ast.ObjectList, performTemplating bool, en pc.Capabilities = []string{DenyCapability} pc.Permissions.CapabilitiesBitmap = DenyCapabilityInt goto PathFinished - case CreateCapability, ReadCapability, UpdateCapability, DeleteCapability, ListCapability, SudoCapability, PatchCapability: + case CreateCapability, ReadCapability, UpdateCapability, DeleteCapability, ListCapability, SudoCapability, PatchCapability, ScanCapability: pc.Permissions.CapabilitiesBitmap |= cap2Int[cap] default: return fmt.Errorf("path %q: invalid capability %q", key, cap) diff --git a/vault/policy_test.go b/vault/policy_test.go index b2bc9a270d..72343924eb 100644 --- a/vault/policy_test.go +++ b/vault/policy_test.go @@ -89,6 +89,9 @@ path "test/req" { path "test/patch" { capabilities = ["patch"] } +path "test/scan" { + capabilities = ["scan"] +} path "test/mfa" { capabilities = ["create", "sudo"] mfa_methods = ["my_totp", "my_totp2"] @@ -257,6 +260,13 @@ func TestPolicy_Parse(t *testing.T) { CapabilitiesBitmap: (PatchCapabilityInt), }, }, + { + Path: "test/scan", + Capabilities: []string{"scan"}, + Permissions: &ACLPermissions{ + CapabilitiesBitmap: (ScanCapabilityInt), + }, + }, { Path: "test/mfa", Capabilities: []string{ diff --git a/website/content/api-docs/secret/kv/kv-v1.mdx b/website/content/api-docs/secret/kv/kv-v1.mdx index efebc3b4e4..b6fcdaf843 100644 --- a/website/content/api-docs/secret/kv/kv-v1.mdx +++ b/website/content/api-docs/secret/kv/kv-v1.mdx @@ -69,9 +69,12 @@ value. Note that no policy-based filtering is performed on keys; do not encode sensitive information in key names. The values themselves are not accessible via this API. +This endpoint also supports recursive listing (scanning). + | Method | Path | | :----- | :-------------- | | `LIST` | `/secret/:path` | +| `SCAN` | `/secret/:path` | ### Parameters diff --git a/website/content/api-docs/secret/kv/kv-v2.mdx b/website/content/api-docs/secret/kv/kv-v2.mdx index a06ce63cea..ab41c62c63 100644 --- a/website/content/api-docs/secret/kv/kv-v2.mdx +++ b/website/content/api-docs/secret/kv/kv-v2.mdx @@ -508,9 +508,12 @@ value. Note that no policy-based filtering is performed on keys; do not encode sensitive information in key names. The values themselves are not accessible via this command. +This endpoint also supports recursive listing (scanning). + | Method | Path | |:-------|:-------------------------------------| | `LIST` | `/:secret-mount-path/metadata/:path` | +| `SCAN` | `/:secret-mount-path/metadata/:path` | ### Parameters diff --git a/website/content/blog/2024-12-11-rfcs-dec-2024.md b/website/content/blog/2024-12-11-rfcs-dec-2024.md new file mode 100644 index 0000000000..b2a35fdf82 --- /dev/null +++ b/website/content/blog/2024-12-11-rfcs-dec-2024.md @@ -0,0 +1,65 @@ +--- +title: "December OpenBao RFC Update" +description: "Overview of in-progress OpenBao RFCs and their status" +slug: rfcs-dec-2024 +authors: cipherboy +tags: [community, mentee, rfcs] +--- + +The second half of 2024 saw several fabulous RFCs from different contributors to OpenBao. Here's a few worth highlighting and how you can get involved! + + + +## In-Progress + +### [openbao#697 / vault#17189 - SSH CA Multi-Issuer](https://github.com/openbao/openbao/issues/679) + +**Description**: Vault and OpenBao have [long had issues](https://github.com/hashicorp/vault/issues/17189) supporting proper rotation of CAs in both the SSH and PKI engines. Previously, an operator would either have to remount an entirely new engine, manually copying configuration and policy, or they'd have to destructively remove the existing CA and create a new one, creating a downtime window. While PKI got updates to add multi-issuer support [in Vault v1.11](https://developer.hashicorp.com/vault/docs/release-notes/1.11.0#improved-ca-rotation), neither Vault nor OpenBao saw improvements to SSH CA rotation. This RFC by one our OpenBao Mentees, [Gabriel](https://github.com/Gabrielopesantos), brings parity by adding multi-issuer support to the SSH engine for zero-downtime CA rotation. + +**How you can help**: Gabriel is working hard on the implementation for this feature, but we'd appreciate feedback on the design from any users of the SSH CA! + +### [openbao#753 - CEL for PKI Policy](https://github.com/openbao/openbao/issues/753) + +**Description**: The PKI Engine's role-based certificate validation method is inflexible: every possible certificate field and extension, complete with desired validation mechanism must be implemented in the engine itself, requiring a release for users to adopt. For instance, `allowed_domains` on a role today doesn't support setting default domains or rejecting requests with only a partial list of allowed domains. While functionally equivalent (in that, a user could request multiple certificates each with a subset of approved domains), it is hard for a PKI operator to ensure that all approved domains have been issued for in a single certificate. Fatima, another OpenBao Mentee, proposes to use Google's Common Expression Language (CEL), widely used in GCP and Kubernetes for validation and templating, to enforce issuance policy and template the final certificate from request parameters. + +**How you can help**: Have experience using CEL or doing complex, company-specific PKI integrations? We'd love to hear from you about the proposed design! + +### [openbao#787 - Add Namespace Support](https://github.com/openbao/openbao/issues/787) + +**Description**: Vault Enterpise supports Namespaces, a way of creating multi-tenancy and delegating permissions without running multiple clusters. [Users](https://lists.lfedge.org/g/OpenBao-TSC/topic/openbao_dev_wg_proposal_to/108266694) [have](https://github.com/openbao/openbao/issues/486) [requested](https://github.com/orgs/openbao/discussions/293) similar abilities with OpenBao, so a temporary working group was formed to create the design and initial implementation. This RFC, published by [Peter](https://github.com/genelet/), proposes API compatibility for consuming applications but suggests many future improvements to scalability and tenant isolation. + +**How you can help**: While the initial implementation will be done by the Namespace WG, we welcome feedback on the design, testing of the feature, and designs and implementations for future enhancements. + +### [openbao#549 / vault#5275 - Recursively List Keys](https://github.com/openbao/openbao/issues/549) + +**Description**: The [most widely requested Vault feature](https://github.com/hashicorp/vault/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc) with no solution [is adding the ability to recursively list keys](https://github.com/hashicorp/vault/issues/5275). Prior to [transactions](https://openbao.org/docs/rfcs/transactions/) (for consistency) and [pagination](https://openbao.org/docs/rfcs/paginated-lists/) (for resource constraining expensive calls), this was hard to do safely. This RFC proposes introducing a new operation, with HTTP verb `SCAN` or via `GET` with `?scan=true`, and ACL capability (`scan`) to allow plugin authors to introduce recursive list endpoints and operators to secure access to them. + +**How you can help**: After the initial implementation is merged, it would be great to have feedback or PRs on additional endpoints to use this new operation. + +### ACL Improvements - [openbao#769 / vault#5362 - Filter LIST Results](https://github.com/openbao/openbao/issues/769) and [openbao#791 - Enforce List Pagination](https://github.com/openbao/openbao/issues/791) + +**Description**: The [fourth-most widely requested Vault feature](https://github.com/hashicorp/vault/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc) with no solution [is filtering LIST results](https://github.com/openbao/openbao/issues/769) to only show accessible paths. [This RFC](https://github.com/openbao/openbao/issues/769) proposes a new ACL policy parameter, `list_scan_response_keys_filter_path`, which contains a path to template with each list response item (from `.keys`) to check against the ACL system for visibility under the same token policy. While this is an expensive operation, a [follow-up RFC](https://github.com/openbao/openbao/issues/791) proposes another parameter, `pagination_limit`, to allow policy authors to require usage of paginated lists (thereby reducing the load on path filtering). + +**How you can help**: It would be great to have feedback on the templating design and how support for multiple paths could potentially behave (with an `AND` or `OR` conjunctions). + +## Completed + +### [openbao#296 - Transactional Storage](https://github.com/openbao/openbao/issues/432) + +**Description**: Recently merged and released in [v2.1.0](https://openbao.org/docs/release-notes/2-1-0/) was support for transactional storage across the entire OpenBao stack (from underlying physical storage backend to plugins). This allowed us to [improve the scalability](https://github.com/openbao/openbao/issues/432) and fault-tolerance of OpenBao above and beyond what HashiCorp Vault had. + +**How you can help**: A follow-up [tracking issue](https://github.com/openbao/openbao/issues/607) invites contributions of places where transactions should be used. As always, we welcome testing of this feature. + +## Upcoming + +### [openbao#235 - Add XChaCha20-Poly1305 Barrier Encryption Support](https://github.com/openbao/openbao/issues/235) + +**Description**: OpenBao uses AES256-GCM96 as its barrier encryption algorithm. While suitably secure and FIPS compliant, this requires frequent key rotation to compensate for the 96-bit nonce (which is not collision resistant). Switching to XChaCha20-Poly1305 would allow us to maintain a smaller barrier keyring (as key rotation would not need to be done automatically) and avoid concerns over nonce collisions. + +**How you can help**: While a proof of concept was proposed, we're looking for a volunteer to take over polishing the feature! + +### [openbao#17 - Add ACME Support to the TLS Listener](https://github.com/openbao/openbao/issues/17) + +**Description**: While OpenBao supports PKI capabilities, due to a chicken-and-egg problem, it is hard to issue the TLS listener's own certificate via a CA stored in OpenBao. With auto-unseal and ACME support, however, it would be possible to do and greatly improve operator's experience when using other CAs as well. + +**How you can help**: While a proof of concept was proposed, much polish was needed to complete this feature and we're looking for a volunteer to take over development! diff --git a/website/content/docs/concepts/policies.mdx b/website/content/docs/concepts/policies.mdx index 33ba2080eb..977db49c5b 100644 --- a/website/content/docs/concepts/policies.mdx +++ b/website/content/docs/concepts/policies.mdx @@ -102,7 +102,7 @@ Here is a more detailed policy, and it is documented inline: # This section grants all access on "secret/*". further restrictions can be # applied to this broad policy, as shown below. path "secret/*" { - capabilities = ["create", "read", "update", "patch", "delete", "list"] + capabilities = ["create", "read", "update", "patch", "delete", "list", "scan"] } # Even though we allowed secret/*, this line explicitly denies @@ -200,9 +200,9 @@ It _is not a regular expression_ and is only supported **as the last character o ::: -When providing `list` capability, it is important to note that since listing -always operates on a prefix, policies must operate on a prefix because OpenBao -will sanitize request paths to be prefixes. +When providing `list` or `scan` capabilities, it is important to note that +since listing always operates on a prefix, policies must operate on a prefix +because OpenBao will sanitize request paths to be prefixes. ### Capabilities @@ -235,6 +235,11 @@ The list of capabilities include the following: keys returned by a `list` operation are _not_ filtered by policies. Do not encode sensitive information in key names. Not all backends support listing. +- `scan` (`SCAN`) - Allows recursively listing ("scanning") values at the + given path. Note that the keys returned by a `list` operation are _not_ + filtered by policies. Do not encode sensitive information in key names. Not + all backends support listing. + In the list above, the associated HTTP verbs are shown in parenthesis next to the capability. When authoring policy, it is usually helpful to look at the HTTP API documentation for the paths and HTTP verbs and map them back onto diff --git a/website/content/docs/rfcs/index.mdx b/website/content/docs/rfcs/index.mdx index e50b1e0172..fe048036c3 100644 --- a/website/content/docs/rfcs/index.mdx +++ b/website/content/docs/rfcs/index.mdx @@ -24,3 +24,6 @@ Steering Committee. storage semantics in `physical.Backend` and `logical.Storage` for use by Core and plugins. This landed in several parts concluding in [PR #292](https://github.com/openbao/openbao/pull/292). + - [SCAN operation](/docs/rfcs/scan-operation), for supporting recursive + lists as a native operation and as an ACL capability. This landed in + [PR #763](https://github.com/openbao/openbao/pull/763). diff --git a/website/content/docs/rfcs/scan-operation.mdx b/website/content/docs/rfcs/scan-operation.mdx new file mode 100644 index 0000000000..b1ad3ac204 --- /dev/null +++ b/website/content/docs/rfcs/scan-operation.mdx @@ -0,0 +1,80 @@ +--- +sidebar_label: SCAN operation +description: |- + An OpenBao RFC to add a recursive list (SCAN) operation and ACL capability. +--- + +# SCAN operation + +## Summary + +Introduce a new ACL capability, `scan`, and operation type, under the `SCAN` HTTP verb or `GET` with `?scan=true`, to safely support recursive listing of entries under a given path. + +## Problem Statement + +Many users operate a K/V mount with a nested, hierarchical entry layout. While the total number of [visible entries](https://github.com/openbao/openbao/issues/769) may not be that many, it may be difficult to navigate even a shallow hierarchy to find the correct entries. This makes supporting a recursive list operation, especially in conjunction with [pruning of non-accessible results](https://github.com/openbao/openbao/issues/769) attractive for a flatter layout. + +Similarly, applications operating over larger datasets (say, for compliance) may want a point-in-time snapshot of all entries within a mount, say, to enqueue auditing of `custom_metadata` with company policies for structure. + +OpenBao has lacked recursive listing of entries from an API perspective, but has had an underlying implementation of this in certain areas via the `logical.ScanView(...)` helper. With the combination of pagination and transactional storage, this becomes consistent and resource constrainable that makes implementing worthwhile. + +## User-facing Description + +For users, OpenBao is introducing a new operation type, under the `SCAN` HTTP verb or `GET` with `?scan=true`, that plugins can implement to indicate that their lists are recursive, if they support hierarchical storage (e.g., of K/V entries versus the flat role list of PKI). Like LIST, this returns all entries within the mount, recursively. The exact implementation details are left up to plugins; please see their documentation for more information. Like LIST, in places where `ListResponseWithInfo(...)` is used, `SCAN` can use the same response format to attach detailed metadata to list entries. + +For operators, OpenBao will allow safely constraining these values by adding a new ACL capability, `scan`, to support limiting users' ability to call these types of endpoints. Like all capabilities, we default to deny behavior and thus users will not get access to these endpoints automatically. + +## Technical Description + +`SCAN` behaves like `LIST` in that it returns all entries within the mount. Presuambly `SCAN` would only be used in plugins which support a `LIST` parameterized with a `:prefix` parameter usually in the URL for ACLing. SCAN would, like LIST, return all entries (albeit, recursively) even if they were not necessarily visible to the caller. The operator would grant explicit access to SCAN results, giving intent to recursively list all entries below the given path. + +However, access to a given prefix's SCAN does not necessarily mean READ access was granted nor that SCAN was granted on sub-paths. E.g., if an operator had an ACL like: + +```hcl +path "secrets/metadata" { + capabilities = ["scan"] +} +``` + +the user would be able to see all entries in the K/V mount. However, they would not be able to call `SCAN secrets/metadata/subpath/` (even though these would show up in the results for `SCAN secrets/metadata/`) or `READ secrets/metadata/some-key`. + +SCAN thus behaves exactly like LIST in that regard. + +Because SCAN is recursive, it will presumably not include directories in its output as there is no need to explicitly call out directories, unlike with LIST. For example, given `a/b` and `c/d`, the output would be `keys: ["a/b", "c/d"]` and not `keys: ["a/", "a/b", "c/", "c/d"]`. + +## Rationale and Alternatives + +One alternative was supporting a `recurse=true` parameter. However, many endpoints which would support a recurse operation already support `LIST` and would require different implementations `storage.List(...)` versus a `logical.ScanView(...)`. This means a separate operation would be more ideal than reusing the existing `LIST` operation. Using a new verb and capability also allows for easier, clearer ACL policies: allowing `list` (without a `denied_parameters=["recurse"]`) would allow recursion, which is not ideal. + +A similar argument goes for pushing this onto plugin developers via separate endpoints. An operator could accidentally grant LIST with recursion on an alternative endpoint without fully understanding the resource implications of it. + +## Downsides + +SCAN is a more expensive operation. However, with `required_parameters=limit` (and potential future improvements to allow policy authors to numerically constrain this value), operators should be able to achieve comparable performance to limited LISTs. + +## Security Implications + +The security implications are mostly the same as LIST, with the extra overhead of recursion. However, this is mitigated by adding a new ACL capability and placing them on unique operation handlers and by potential future work to enforce numericial `limit` constraints. + +## User/Developer Experience + +This helps certain use cases as enumeated above, especially when humans or automated systems are directly interacting with OpenBao. + +For consumers of OpenAPI generation, this will [have some impact](https://github.com/orgs/openbao/discussions/656) where `LIST`, `GET`, and `SCAN` are used at the same time, but a similar workaround to the existing LIST workaround would suffice here. However, due to the `GET` fallback, this should otherwise be accessible everywhere `LIST` is. + +## Unresolved Questions + +n/a + +## Related Issues + + - https://github.com/openbao/openbao/issues/769 implements response filtering for this. + - https://github.com/openbao/openbao/issues/549 is a feature request asking for this for K/V. + +Upstream: + + - https://github.com/hashicorp/vault/issues/5275 + +## Proof of Concept + +https://github.com/openbao/openbao/pull/763 is the open pull request for this change. diff --git a/website/sidebars.ts b/website/sidebars.ts index a2823f1c73..1760e03dc1 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -492,6 +492,7 @@ const sidebars: SidebarsConfig = { "rfcs/signed-commits", "rfcs/transactions", "rfcs/split-mount-tables", + "rfcs/scan-operation", ], FAQ: ["faq/index", "deprecation/faq", "auth/login-mfa/faq"], },