From b936db8332f647e75449708ae4c938f9e42b15af Mon Sep 17 00:00:00 2001 From: Jordan Reimer Date: Thu, 17 Feb 2022 13:17:59 -0700 Subject: [PATCH] Revert "MFA (#14049)" (#14135) This reverts commit 5f17953b5980e6438215d5cb62c8575d16c63193. --- api/client.go | 6 +- api/sys_mounts.go | 91 +++++++- builtin/credential/approle/path_login.go | 9 +- builtin/logical/ssh/path_config_ca.go | 9 +- builtin/logical/ssh/path_config_ca_test.go | 36 +++- builtin/logical/transit/backend.go | 8 +- builtin/logical/transit/backend_test.go | 6 +- builtin/logical/transit/path_config.go | 14 +- builtin/logical/transit/path_config_test.go | 56 ++--- builtin/logical/transit/path_keys.go | 12 +- builtin/logical/transit/path_keys_test.go | 42 ++-- changelog/14049.txt | 3 - changelog/14067.txt | 3 + changelog/14107.txt | 3 + command/secrets_move.go | 14 +- command/secrets_move_test.go | 7 +- go.mod | 1 + go.sum | 2 + helper/namespace/namespace.go | 17 ++ http/sys_mount_test.go | 20 +- sdk/helper/keysutil/lock_manager.go | 4 +- sdk/helper/keysutil/policy.go | 4 +- ui/app/adapters/cluster.js | 13 -- ui/app/components/auth-form.js | 76 +++++-- ui/app/components/clients/current.js | 47 ++++- ui/app/components/clients/history.js | 72 +++++-- ui/app/components/date-dropdown.js | 17 +- ui/app/components/mfa-error.js | 43 ---- ui/app/components/mfa-form.js | 89 -------- ui/app/components/transit-edit.js | 4 +- ui/app/controllers/vault/cluster/auth.js | 46 +--- ui/app/helpers/format-duration.js | 6 +- ui/app/helpers/number-to-word.js | 22 -- ui/app/models/transit-key.js | 4 +- .../routes/vault/cluster/clients/history.js | 7 +- ui/app/serializers/clients/config.js | 2 +- ui/app/serializers/transit-key.js | 4 +- ui/app/services/auth.js | 92 +------- ui/app/styles/components/icon.scss | 4 - ui/app/styles/core/buttons.scss | 10 +- ui/app/styles/core/helpers.scss | 10 +- .../templates/components/clients/current.hbs | 13 ++ .../templates/components/clients/history.hbs | 58 +++-- ui/app/templates/components/date-dropdown.hbs | 16 +- ui/app/templates/components/mfa-error.hbs | 15 -- ui/app/templates/components/mfa-form.hbs | 70 ------ ui/app/templates/components/splash-page.hbs | 35 ++- .../components/transit-form-create.hbs | 2 +- .../components/transit-form-edit.hbs | 6 +- .../components/transit-form-show.hbs | 4 +- ui/app/templates/vault/cluster/auth.hbs | 127 +++++------ .../templates/vault/cluster/clients/error.hbs | 29 +++ ui/lib/core/addon/components/select.js | 20 +- .../addon/templates/components/select.hbs | 5 - ui/mirage/factories/mfa-method.js | 12 -- ui/mirage/handlers/activity.js | 63 +++++- ui/mirage/handlers/index.js | 3 +- ui/mirage/handlers/mfa.js | 146 ------------- ui/stories/auth-form.md | 22 +- ui/tests/acceptance/auth-test.js | 8 +- ui/tests/acceptance/mfa-test.js | 135 ------------ .../integration/components/auth-form-test.js | 2 - .../integration/components/mfa-error-test.js | 38 ---- .../integration/components/mfa-form-test.js | 190 ----------------- .../integration/helpers/format-ttl-test.js | 8 + ui/tests/pages/auth.js | 2 - vault/auth.go | 7 +- vault/core.go | 5 + vault/logical_system.go | 144 +++++++++++-- vault/logical_system_paths.go | 53 +++-- vault/logical_system_quotas.go | 4 +- vault/logical_system_test.go | 30 +-- vault/mount.go | 199 ++++++++++++------ vault/mount_test.go | 10 +- vault/quotas/quotas.go | 85 +++++--- vault/quotas/quotas_rate_limit.go | 7 +- vault/quotas/quotas_test.go | 2 +- vault/quotas/quotas_util.go | 6 +- website/content/api-docs/secret/transit.mdx | 6 +- website/content/docs/plugin-portal.mdx | 3 +- 80 files changed, 1123 insertions(+), 1402 deletions(-) delete mode 100644 changelog/14049.txt create mode 100644 changelog/14067.txt create mode 100644 changelog/14107.txt delete mode 100644 ui/app/components/mfa-error.js delete mode 100644 ui/app/components/mfa-form.js delete mode 100644 ui/app/helpers/number-to-word.js delete mode 100644 ui/app/templates/components/mfa-error.hbs delete mode 100644 ui/app/templates/components/mfa-form.hbs create mode 100644 ui/app/templates/vault/cluster/clients/error.hbs delete mode 100644 ui/mirage/factories/mfa-method.js delete mode 100644 ui/mirage/handlers/mfa.js delete mode 100644 ui/tests/acceptance/mfa-test.js delete mode 100644 ui/tests/integration/components/mfa-error-test.js delete mode 100644 ui/tests/integration/components/mfa-form-test.js diff --git a/api/client.go b/api/client.go index 9f84dd83d..87f8c537e 100644 --- a/api/client.go +++ b/api/client.go @@ -51,6 +51,8 @@ const ( EnvRateLimit = "VAULT_RATE_LIMIT" EnvHTTPProxy = "VAULT_HTTP_PROXY" HeaderIndex = "X-Vault-Index" + HeaderForward = "X-Vault-Forward" + HeaderInconsistent = "X-Vault-Inconsistent" ) // Deprecated values @@ -1395,7 +1397,7 @@ func ParseReplicationState(raw string, hmacKey []byte) (*logical.WALState, error // conjunction with RequireState. func ForwardInconsistent() RequestCallback { return func(req *Request) { - req.Headers.Set("X-Vault-Inconsistent", "forward-active-node") + req.Headers.Set(HeaderInconsistent, "forward-active-node") } } @@ -1404,7 +1406,7 @@ func ForwardInconsistent() RequestCallback { // This feature must be enabled in Vault's configuration. func ForwardAlways() RequestCallback { return func(req *Request) { - req.Headers.Set("X-Vault-Forward", "active-node") + req.Headers.Set(HeaderForward, "active-node") } } diff --git a/api/sys_mounts.go b/api/sys_mounts.go index 1d68a1063..8a0c5b985 100644 --- a/api/sys_mounts.go +++ b/api/sys_mounts.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "time" "github.com/mitchellh/mapstructure" ) @@ -65,7 +66,31 @@ func (c *Sys) Unmount(path string) error { return err } +// Remount kicks off a remount operation, polls the status endpoint using +// the migration ID till either success or failure state is observed func (c *Sys) Remount(from, to string) error { + remountResp, err := c.StartRemount(from, to) + if err != nil { + return err + } + + for { + remountStatusResp, err := c.RemountStatus(remountResp.MigrationID) + if err != nil { + return err + } + if remountStatusResp.MigrationInfo.MigrationStatus == "success" { + return nil + } + if remountStatusResp.MigrationInfo.MigrationStatus == "failure" { + return fmt.Errorf("Failure! Error encountered moving mount %s to %s, with migration ID %s", from, to, remountResp.MigrationID) + } + time.Sleep(1 * time.Second) + } +} + +// StartRemount kicks off a mount migration and returns a response with the migration ID +func (c *Sys) StartRemount(from, to string) (*MountMigrationOutput, error) { body := map[string]interface{}{ "from": from, "to": to, @@ -73,16 +98,59 @@ func (c *Sys) Remount(from, to string) error { r := c.c.NewRequest("POST", "/v1/sys/remount") if err := r.SetJSONBody(body); err != nil { - return err + return nil, err } ctx, cancelFunc := context.WithCancel(context.Background()) defer cancelFunc() resp, err := c.c.RawRequestWithContext(ctx, r) - if err == nil { - defer resp.Body.Close() + if err != nil { + return nil, err } - return err + defer resp.Body.Close() + secret, err := ParseSecret(resp.Body) + if err != nil { + return nil, err + } + if secret == nil || secret.Data == nil { + return nil, errors.New("data from server response is empty") + } + + var result MountMigrationOutput + err = mapstructure.Decode(secret.Data, &result) + if err != nil { + return nil, err + } + + return &result, err +} + +// RemountStatus checks the status of a mount migration operation with the provided ID +func (c *Sys) RemountStatus(migrationID string) (*MountMigrationStatusOutput, error) { + r := c.c.NewRequest("GET", fmt.Sprintf("/v1/sys/remount/status/%s", migrationID)) + + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + resp, err := c.c.RawRequestWithContext(ctx, r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + secret, err := ParseSecret(resp.Body) + if err != nil { + return nil, err + } + if secret == nil || secret.Data == nil { + return nil, errors.New("data from server response is empty") + } + + var result MountMigrationStatusOutput + err = mapstructure.Decode(secret.Data, &result) + if err != nil { + return nil, err + } + + return &result, err } func (c *Sys) TuneMount(path string, config MountConfigInput) error { @@ -187,3 +255,18 @@ type MountConfigOutput struct { // Deprecated: This field will always be blank for newer server responses. PluginName string `json:"plugin_name,omitempty" mapstructure:"plugin_name"` } + +type MountMigrationOutput struct { + MigrationID string `mapstructure:"migration_id"` +} + +type MountMigrationStatusOutput struct { + MigrationID string `mapstructure:"migration_id"` + MigrationInfo *MountMigrationStatusInfo `mapstructure:"migration_info"` +} + +type MountMigrationStatusInfo struct { + SourceMount string `mapstructure:"source_mount"` + TargetMount string `mapstructure:"target_mount"` + MigrationStatus string `mapstructure:"status"` +} diff --git a/builtin/credential/approle/path_login.go b/builtin/credential/approle/path_login.go index a392966fa..ba478c4ff 100644 --- a/builtin/credential/approle/path_login.go +++ b/builtin/credential/approle/path_login.go @@ -178,11 +178,14 @@ func (b *backend) pathLoginUpdate(ctx context.Context, req *logical.Request, dat } belongs, err := cidrutil.IPBelongsToCIDRBlocksSlice(req.Connection.RemoteAddr, entry.CIDRList) - if !belongs || err != nil { + if err != nil { + return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest + } + + if !belongs { return logical.ErrorResponse(fmt.Errorf( - "source address %q unauthorized through CIDR restrictions on the secret ID: %w", + "source address %q unauthorized through CIDR restrictions on the secret ID", req.Connection.RemoteAddr, - err, ).Error()), nil } } diff --git a/builtin/logical/ssh/path_config_ca.go b/builtin/logical/ssh/path_config_ca.go index 42ae388a6..5b7593938 100644 --- a/builtin/logical/ssh/path_config_ca.go +++ b/builtin/logical/ssh/path_config_ca.go @@ -10,6 +10,7 @@ import ( "crypto/rsa" "crypto/x509" "encoding/pem" + "errors" "fmt" "io" @@ -17,6 +18,8 @@ import ( "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" "golang.org/x/crypto/ssh" + + "github.com/mikesmitty/edkey" ) const ( @@ -357,9 +360,9 @@ func generateSSHKeyPair(randomSource io.Reader, keyType string, keyBits int) (st return "", "", err } - marshalled, err := x509.MarshalPKCS8PrivateKey(privateSeed) - if err != nil { - return "", "", err + marshalled := edkey.MarshalED25519PrivateKey(privateSeed) + if marshalled == nil { + return "", "", errors.New("unable to marshal ed25519 private key") } privateBlock = &pem.Block{ diff --git a/builtin/logical/ssh/path_config_ca_test.go b/builtin/logical/ssh/path_config_ca_test.go index d346c5710..1a04d9dbe 100644 --- a/builtin/logical/ssh/path_config_ca_test.go +++ b/builtin/logical/ssh/path_config_ca_test.go @@ -191,17 +191,31 @@ func createDeleteHelper(t *testing.T, b logical.Backend, config *logical.Backend } resp, err := b.HandleRequest(context.Background(), caReq) if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad case %v: err: %v, resp:%v", index, err, resp) + t.Fatalf("bad case %v: err: %v, resp: %v", index, err, resp) } if !strings.Contains(resp.Data["public_key"].(string), caReq.Data["key_type"].(string)) { t.Fatalf("bad case %v: expected public key of type %v but was %v", index, caReq.Data["key_type"], resp.Data["public_key"]) } + issueOptions := map[string]interface{}{ + "public_key": testCAPublicKeyEd25519, + } + issueReq := &logical.Request{ + Path: "sign/ca-issuance", + Operation: logical.UpdateOperation, + Storage: config.StorageView, + Data: issueOptions, + } + resp, err = b.HandleRequest(context.Background(), issueReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("bad case %v: err: %v, resp: %v", index, err, resp) + } + // Delete the configured keys caReq.Operation = logical.DeleteOperation resp, err = b.HandleRequest(context.Background(), caReq) if err != nil || (resp != nil && resp.IsError()) { - t.Fatalf("bad case %v: err: %v, resp:%v", index, err, resp) + t.Fatalf("bad case %v: err: %v, resp: %v", index, err, resp) } } @@ -235,6 +249,24 @@ func TestSSH_ConfigCAKeyTypes(t *testing.T) { {"ed25519", 0}, } + // Create a role for ssh signing. + roleOptions := map[string]interface{}{ + "allow_user_certificates": true, + "allowed_users": "*", + "key_type": "ca", + "ttl": "30s", + } + roleReq := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "roles/ca-issuance", + Data: roleOptions, + Storage: config.StorageView, + } + _, err = b.HandleRequest(context.Background(), roleReq) + if err != nil { + t.Fatalf("Cannot create role to issue against: %s", err) + } + for index, scenario := range cases { createDeleteHelper(t, b, config, index, scenario.keyType, scenario.keyBits) } diff --git a/builtin/logical/transit/backend.go b/builtin/logical/transit/backend.go index 3cab23fc6..acc714d5f 100644 --- a/builtin/logical/transit/backend.go +++ b/builtin/logical/transit/backend.go @@ -190,7 +190,7 @@ func (b *backend) periodicFunc(ctx context.Context, req *logical.Request) error } // autoRotateKeys retrieves all transit keys and rotates those which have an -// auto rotate interval defined which has passed. This operation only happens +// auto rotate period defined which has passed. This operation only happens // on primary nodes and performance secondary nodes which have a local mount. func (b *backend) autoRotateKeys(ctx context.Context, req *logical.Request) error { // Only check for autorotation once an hour to avoid unnecessarily iterating @@ -247,15 +247,15 @@ func (b *backend) rotateIfRequired(ctx context.Context, req *logical.Request, ke } defer p.Unlock() - // If the policy's automatic rotation interval is 0, it should not + // If the policy's automatic rotation period is 0, it should not // automatically rotate. - if p.AutoRotateInterval == 0 { + if p.AutoRotatePeriod == 0 { return nil } // Retrieve the latest version of the policy and determine if it is time to rotate. latestKey := p.Keys[strconv.Itoa(p.LatestVersion)] - if time.Now().After(latestKey.CreationTime.Add(p.AutoRotateInterval)) { + if time.Now().After(latestKey.CreationTime.Add(p.AutoRotatePeriod)) { if b.Logger().IsDebug() { b.Logger().Debug("automatically rotating key", "key", key) } diff --git a/builtin/logical/transit/backend_test.go b/builtin/logical/transit/backend_test.go index 6ad7b2d7d..c49768e73 100644 --- a/builtin/logical/transit/backend_test.go +++ b/builtin/logical/transit/backend_test.go @@ -1607,7 +1607,7 @@ func TestTransit_AutoRotateKeys(t *testing.T) { Operation: logical.UpdateOperation, Path: "keys/test2", Data: map[string]interface{}{ - "auto_rotate_interval": 24 * time.Hour, + "auto_rotate_period": 24 * time.Hour, }, } resp, err = b.HandleRequest(context.Background(), req) @@ -1651,7 +1651,7 @@ func TestTransit_AutoRotateKeys(t *testing.T) { t.Fatalf("incorrect latest_version found, got: %d, want: %d", resp.Data["latest_version"], 1) } - // Update auto rotate interval on one key to be one nanosecond + // Update auto rotate period on one key to be one nanosecond p, _, err := b.GetPolicy(context.Background(), keysutil.PolicyRequest{ Storage: storage, Name: "test2", @@ -1662,7 +1662,7 @@ func TestTransit_AutoRotateKeys(t *testing.T) { if p == nil { t.Fatal("expected non-nil policy") } - p.AutoRotateInterval = time.Nanosecond + p.AutoRotatePeriod = time.Nanosecond err = p.Persist(context.Background(), storage) if err != nil { t.Fatal(err) diff --git a/builtin/logical/transit/path_config.go b/builtin/logical/transit/path_config.go index 336643227..a3d72731b 100644 --- a/builtin/logical/transit/path_config.go +++ b/builtin/logical/transit/path_config.go @@ -49,7 +49,7 @@ the latest version of the key is allowed.`, Description: `Enables taking a backup of the named key in plaintext format. Once set, this cannot be disabled.`, }, - "auto_rotate_interval": { + "auto_rotate_period": { Type: framework.TypeDurationSecond, Description: `Amount of time the key should live before being automatically rotated. A value of 0 @@ -193,19 +193,19 @@ func (b *backend) pathConfigWrite(ctx context.Context, req *logical.Request, d * } } - autoRotateIntervalRaw, ok, err := d.GetOkErr("auto_rotate_interval") + autoRotatePeriodRaw, ok, err := d.GetOkErr("auto_rotate_period") if err != nil { return nil, err } if ok { - autoRotateInterval := time.Second * time.Duration(autoRotateIntervalRaw.(int)) + autoRotatePeriod := time.Second * time.Duration(autoRotatePeriodRaw.(int)) // Provided value must be 0 to disable or at least an hour - if autoRotateInterval != 0 && autoRotateInterval < time.Hour { - return logical.ErrorResponse("auto rotate interval must be 0 to disable or at least an hour"), nil + if autoRotatePeriod != 0 && autoRotatePeriod < time.Hour { + return logical.ErrorResponse("auto rotate period must be 0 to disable or at least an hour"), nil } - if autoRotateInterval != p.AutoRotateInterval { - p.AutoRotateInterval = autoRotateInterval + if autoRotatePeriod != p.AutoRotatePeriod { + p.AutoRotatePeriod = autoRotatePeriod persistNeeded = true } } diff --git a/builtin/logical/transit/path_config_test.go b/builtin/logical/transit/path_config_test.go index 3888bc861..bc5843c65 100644 --- a/builtin/logical/transit/path_config_test.go +++ b/builtin/logical/transit/path_config_test.go @@ -294,44 +294,44 @@ func TestTransit_ConfigSettings(t *testing.T) { func TestTransit_UpdateKeyConfigWithAutorotation(t *testing.T) { tests := map[string]struct { - initialAutoRotateInterval interface{} - newAutoRotateInterval interface{} - shouldError bool - expectedValue time.Duration + initialAutoRotatePeriod interface{} + newAutoRotatePeriod interface{} + shouldError bool + expectedValue time.Duration }{ "default (no value)": { - initialAutoRotateInterval: "5h", - shouldError: false, - expectedValue: 5 * time.Hour, + initialAutoRotatePeriod: "5h", + shouldError: false, + expectedValue: 5 * time.Hour, }, "0 (int)": { - initialAutoRotateInterval: "5h", - newAutoRotateInterval: 0, - shouldError: false, - expectedValue: 0, + initialAutoRotatePeriod: "5h", + newAutoRotatePeriod: 0, + shouldError: false, + expectedValue: 0, }, "0 (string)": { - initialAutoRotateInterval: "5h", - newAutoRotateInterval: 0, - shouldError: false, - expectedValue: 0, + initialAutoRotatePeriod: "5h", + newAutoRotatePeriod: 0, + shouldError: false, + expectedValue: 0, }, "5 seconds": { - newAutoRotateInterval: "5s", - shouldError: true, + newAutoRotatePeriod: "5s", + shouldError: true, }, "5 hours": { - newAutoRotateInterval: "5h", - shouldError: false, - expectedValue: 5 * time.Hour, + newAutoRotatePeriod: "5h", + shouldError: false, + expectedValue: 5 * time.Hour, }, "negative value": { - newAutoRotateInterval: "-1800s", - shouldError: true, + newAutoRotatePeriod: "-1800s", + shouldError: true, }, "invalid string": { - newAutoRotateInterval: "this shouldn't work", - shouldError: true, + newAutoRotatePeriod: "this shouldn't work", + shouldError: true, }, } @@ -364,11 +364,11 @@ func TestTransit_UpdateKeyConfigWithAutorotation(t *testing.T) { keyName := hex.EncodeToString(keyNameBytes) _, err = client.Logical().Write(fmt.Sprintf("transit/keys/%s", keyName), map[string]interface{}{ - "auto_rotate_interval": test.initialAutoRotateInterval, + "auto_rotate_period": test.initialAutoRotatePeriod, }) resp, err := client.Logical().Write(fmt.Sprintf("transit/keys/%s/config", keyName), map[string]interface{}{ - "auto_rotate_interval": test.newAutoRotateInterval, + "auto_rotate_period": test.newAutoRotatePeriod, }) switch { case test.shouldError && err == nil: @@ -385,7 +385,7 @@ func TestTransit_UpdateKeyConfigWithAutorotation(t *testing.T) { if resp == nil { t.Fatal("expected non-nil response") } - gotRaw, ok := resp.Data["auto_rotate_interval"].(json.Number) + gotRaw, ok := resp.Data["auto_rotate_period"].(json.Number) if !ok { t.Fatal("returned value is of unexpected type") } @@ -395,7 +395,7 @@ func TestTransit_UpdateKeyConfigWithAutorotation(t *testing.T) { } want := int64(test.expectedValue.Seconds()) if got != want { - t.Fatalf("incorrect auto_rotate_interval returned, got: %d, want: %d", got, want) + t.Fatalf("incorrect auto_rotate_period returned, got: %d, want: %d", got, want) } } }) diff --git a/builtin/logical/transit/path_keys.go b/builtin/logical/transit/path_keys.go index b05b28e8c..bcaf326c2 100644 --- a/builtin/logical/transit/path_keys.go +++ b/builtin/logical/transit/path_keys.go @@ -95,7 +95,7 @@ if the key type supports public keys, this will return the public key for the given context.`, }, - "auto_rotate_interval": { + "auto_rotate_period": { Type: framework.TypeDurationSecond, Default: 0, Description: `Amount of time the key should live before @@ -132,10 +132,10 @@ func (b *backend) pathPolicyWrite(ctx context.Context, req *logical.Request, d * keyType := d.Get("type").(string) exportable := d.Get("exportable").(bool) allowPlaintextBackup := d.Get("allow_plaintext_backup").(bool) - autoRotateInterval := time.Second * time.Duration(d.Get("auto_rotate_interval").(int)) + autoRotatePeriod := time.Second * time.Duration(d.Get("auto_rotate_period").(int)) - if autoRotateInterval != 0 && autoRotateInterval < time.Hour { - return logical.ErrorResponse("auto rotate interval must be 0 to disable or at least an hour"), nil + if autoRotatePeriod != 0 && autoRotatePeriod < time.Hour { + return logical.ErrorResponse("auto rotate period must be 0 to disable or at least an hour"), nil } if !derived && convergent { @@ -150,7 +150,7 @@ func (b *backend) pathPolicyWrite(ctx context.Context, req *logical.Request, d * Convergent: convergent, Exportable: exportable, AllowPlaintextBackup: allowPlaintextBackup, - AutoRotateInterval: autoRotateInterval, + AutoRotatePeriod: autoRotatePeriod, } switch keyType { case "aes128-gcm96": @@ -238,7 +238,7 @@ func (b *backend) pathPolicyRead(ctx context.Context, req *logical.Request, d *f "supports_decryption": p.Type.DecryptionSupported(), "supports_signing": p.Type.SigningSupported(), "supports_derivation": p.Type.DerivationSupported(), - "auto_rotate_interval": int64(p.AutoRotateInterval.Seconds()), + "auto_rotate_period": int64(p.AutoRotatePeriod.Seconds()), }, } diff --git a/builtin/logical/transit/path_keys_test.go b/builtin/logical/transit/path_keys_test.go index c74b580d1..04c1d8da0 100644 --- a/builtin/logical/transit/path_keys_test.go +++ b/builtin/logical/transit/path_keys_test.go @@ -95,39 +95,39 @@ func TestTransit_Issue_2958(t *testing.T) { func TestTransit_CreateKeyWithAutorotation(t *testing.T) { tests := map[string]struct { - autoRotateInterval interface{} - shouldError bool - expectedValue time.Duration + autoRotatePeriod interface{} + shouldError bool + expectedValue time.Duration }{ "default (no value)": { shouldError: false, }, "0 (int)": { - autoRotateInterval: 0, - shouldError: false, - expectedValue: 0, + autoRotatePeriod: 0, + shouldError: false, + expectedValue: 0, }, "0 (string)": { - autoRotateInterval: "0", - shouldError: false, - expectedValue: 0, + autoRotatePeriod: "0", + shouldError: false, + expectedValue: 0, }, "5 seconds": { - autoRotateInterval: "5s", - shouldError: true, + autoRotatePeriod: "5s", + shouldError: true, }, "5 hours": { - autoRotateInterval: "5h", - shouldError: false, - expectedValue: 5 * time.Hour, + autoRotatePeriod: "5h", + shouldError: false, + expectedValue: 5 * time.Hour, }, "negative value": { - autoRotateInterval: "-1800s", - shouldError: true, + autoRotatePeriod: "-1800s", + shouldError: true, }, "invalid string": { - autoRotateInterval: "this shouldn't work", - shouldError: true, + autoRotatePeriod: "this shouldn't work", + shouldError: true, }, } @@ -160,7 +160,7 @@ func TestTransit_CreateKeyWithAutorotation(t *testing.T) { keyName := hex.EncodeToString(keyNameBytes) _, err = client.Logical().Write(fmt.Sprintf("transit/keys/%s", keyName), map[string]interface{}{ - "auto_rotate_interval": test.autoRotateInterval, + "auto_rotate_period": test.autoRotatePeriod, }) switch { case test.shouldError && err == nil: @@ -177,7 +177,7 @@ func TestTransit_CreateKeyWithAutorotation(t *testing.T) { if resp == nil { t.Fatal("expected non-nil response") } - gotRaw, ok := resp.Data["auto_rotate_interval"].(json.Number) + gotRaw, ok := resp.Data["auto_rotate_period"].(json.Number) if !ok { t.Fatal("returned value is of unexpected type") } @@ -187,7 +187,7 @@ func TestTransit_CreateKeyWithAutorotation(t *testing.T) { } want := int64(test.expectedValue.Seconds()) if got != want { - t.Fatalf("incorrect auto_rotate_interval returned, got: %d, want: %d", got, want) + t.Fatalf("incorrect auto_rotate_period returned, got: %d, want: %d", got, want) } } }) diff --git a/changelog/14049.txt b/changelog/14049.txt deleted file mode 100644 index 93af683bb..000000000 --- a/changelog/14049.txt +++ /dev/null @@ -1,3 +0,0 @@ -```release-note:improvement -ui: Adds multi-factor authentication support -``` \ No newline at end of file diff --git a/changelog/14067.txt b/changelog/14067.txt new file mode 100644 index 000000000..bd24019ad --- /dev/null +++ b/changelog/14067.txt @@ -0,0 +1,3 @@ +```release-note:improvement +api: Define constants for X-Vault-Forward and X-Vault-Inconsistent headers +``` diff --git a/changelog/14107.txt b/changelog/14107.txt new file mode 100644 index 000000000..f17138c05 --- /dev/null +++ b/changelog/14107.txt @@ -0,0 +1,3 @@ +```release-note:bug +auth/approle: Fix wrapping of nil errors in `login` endpoint +``` diff --git a/command/secrets_move.go b/command/secrets_move.go index a04ec090b..ff3331047 100644 --- a/command/secrets_move.go +++ b/command/secrets_move.go @@ -29,8 +29,8 @@ Usage: vault secrets move [options] SOURCE DESTINATION secrets engine are revoked, but all configuration associated with the engine is preserved. - This command only works within a namespace; it cannot be used to move engines - to different namespaces. + This command works within or across namespaces, both source and destination paths + can be prefixed with a namespace heirarchy relative to the current namespace. WARNING! Moving an existing secrets engine will revoke any leases from the old engine. @@ -39,6 +39,11 @@ Usage: vault secrets move [options] SOURCE DESTINATION $ vault secrets move secret/ generic/ + Move the existing secrets engine at ns1/secret/ across namespaces to ns2/generic/, + where ns1 and ns2 are child namespaces of the current namespace: + + $ vault secrets move ns1/secret/ ns2/generic/ + ` + c.Flags().Help() return strings.TrimSpace(helpText) @@ -84,11 +89,12 @@ func (c *SecretsMoveCommand) Run(args []string) int { return 2 } - if err := client.Sys().Remount(source, destination); err != nil { + remountResp, err := client.Sys().StartRemount(source, destination) + if err != nil { c.UI.Error(fmt.Sprintf("Error moving secrets engine %s to %s: %s", source, destination, err)) return 2 } - c.UI.Output(fmt.Sprintf("Success! Moved secrets engine %s to: %s", source, destination)) + c.UI.Output(fmt.Sprintf("Success! Started moving secrets engine %s to %s, with migration ID %s", source, destination, remountResp.MigrationID)) return 0 } diff --git a/command/secrets_move_test.go b/command/secrets_move_test.go index 1af52d131..bca2a530f 100644 --- a/command/secrets_move_test.go +++ b/command/secrets_move_test.go @@ -3,6 +3,7 @@ package command import ( "strings" "testing" + "time" "github.com/mitchellh/cli" ) @@ -91,12 +92,16 @@ func TestSecretsMoveCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Success! Moved secrets engine secret/ to: generic/" + expected := "Success! Started moving secrets engine secret/ to generic/" combined := ui.OutputWriter.String() + ui.ErrorWriter.String() if !strings.Contains(combined, expected) { t.Errorf("expected %q to contain %q", combined, expected) } + // Wait for the move command to complete. Ideally we'd check remount status + // explicitly but we don't have migration id here + time.Sleep(1 * time.Second) + mounts, err := client.Sys().ListMounts() if err != nil { t.Fatal(err) diff --git a/go.mod b/go.mod index 3689061ee..dea72614b 100644 --- a/go.mod +++ b/go.mod @@ -306,6 +306,7 @@ require ( github.com/mattn/go-isatty v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/miekg/dns v1.1.41 // indirect + github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect github.com/mitchellh/hashstructure v1.0.0 // indirect github.com/mitchellh/iochan v1.0.0 // indirect github.com/mitchellh/pointerstructure v1.2.0 // indirect diff --git a/go.sum b/go.sum index fc331e840..af562945c 100644 --- a/go.sum +++ b/go.sum @@ -1153,6 +1153,8 @@ github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKju github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a h1:eU8j/ClY2Ty3qdHnn0TyW3ivFoPC/0F1gQZz8yTxbbE= +github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a/go.mod h1:v8eSC2SMp9/7FTKUncp7fH9IwPfw+ysMObcEz5FWheQ= github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= diff --git a/helper/namespace/namespace.go b/helper/namespace/namespace.go index b6aba2bc3..6cdaed71c 100644 --- a/helper/namespace/namespace.go +++ b/helper/namespace/namespace.go @@ -133,3 +133,20 @@ func SplitIDFromString(input string) (string, string) { return prefix + input[:idx], input[idx+1:] } + +// MountPathDetails contains the details of a mount's location, +// consisting of the namespace of the mount and the path of the +// mount within the namespace +type MountPathDetails struct { + Namespace *Namespace + MountPath string +} + +func (mpd *MountPathDetails) GetRelativePath(currNs *Namespace) string { + subNsPath := strings.TrimPrefix(mpd.Namespace.Path, currNs.Path) + return subNsPath + mpd.MountPath +} + +func (mpd *MountPathDetails) GetFullPath() string { + return mpd.Namespace.Path + mpd.MountPath +} diff --git a/http/sys_mount_test.go b/http/sys_mount_test.go index 5c5bfabb5..71c454a9e 100644 --- a/http/sys_mount_test.go +++ b/http/sys_mount_test.go @@ -2,8 +2,10 @@ package http import ( "encoding/json" + "fmt" "reflect" "testing" + "time" "github.com/go-test/deep" @@ -374,8 +376,24 @@ func TestSysRemount(t *testing.T) { "from": "foo", "to": "bar", }) - testResponseStatus(t, resp, 204) + testResponseStatus(t, resp, 200) + // Poll until the remount succeeds + var remountResp map[string]interface{} + testResponseBody(t, resp, &remountResp) + vault.RetryUntil(t, 5*time.Second, func() error { + resp = testHttpGet(t, token, addr+"/v1/sys/remount/status/"+remountResp["migration_id"].(string)) + testResponseStatus(t, resp, 200) + + var remountStatusResp map[string]interface{} + testResponseBody(t, resp, &remountStatusResp) + + status := remountStatusResp["data"].(map[string]interface{})["migration_info"].(map[string]interface{})["status"] + if status != "success" { + return fmt.Errorf("Expected migration status to be successful, got %q", status) + } + return nil + }) resp = testHttpGet(t, token, addr+"/v1/sys/mounts") var actual map[string]interface{} diff --git a/sdk/helper/keysutil/lock_manager.go b/sdk/helper/keysutil/lock_manager.go index 71bfcac84..a424ca9e0 100644 --- a/sdk/helper/keysutil/lock_manager.go +++ b/sdk/helper/keysutil/lock_manager.go @@ -52,7 +52,7 @@ type PolicyRequest struct { AllowPlaintextBackup bool // How frequently the key should automatically rotate - AutoRotateInterval time.Duration + AutoRotatePeriod time.Duration } type LockManager struct { @@ -383,7 +383,7 @@ func (lm *LockManager) GetPolicy(ctx context.Context, req PolicyRequest, rand io Derived: req.Derived, Exportable: req.Exportable, AllowPlaintextBackup: req.AllowPlaintextBackup, - AutoRotateInterval: req.AutoRotateInterval, + AutoRotatePeriod: req.AutoRotatePeriod, } if req.Derived { diff --git a/sdk/helper/keysutil/policy.go b/sdk/helper/keysutil/policy.go index d4b82ab82..59afa99fd 100644 --- a/sdk/helper/keysutil/policy.go +++ b/sdk/helper/keysutil/policy.go @@ -374,9 +374,9 @@ type Policy struct { // policy object. StoragePrefix string `json:"storage_prefix"` - // AutoRotateInterval defines how frequently the key should automatically + // AutoRotatePeriod defines how frequently the key should automatically // rotate. Setting this to zero disables automatic rotation for the key. - AutoRotateInterval time.Duration `json:"auto_rotate_interval"` + AutoRotatePeriod time.Duration `json:"auto_rotate_period"` // versionPrefixCache stores caches of version prefix strings and the split // version template. diff --git a/ui/app/adapters/cluster.js b/ui/app/adapters/cluster.js index f86004426..4b52350b1 100644 --- a/ui/app/adapters/cluster.js +++ b/ui/app/adapters/cluster.js @@ -126,19 +126,6 @@ export default ApplicationAdapter.extend({ return this.ajax(url, verb, options); }, - mfaValidate({ mfa_request_id, mfa_constraints }) { - const options = { - data: { - mfa_request_id, - mfa_payload: mfa_constraints.reduce((obj, { selectedMethod, passcode }) => { - obj[selectedMethod.id] = passcode ? [passcode] : []; - return obj; - }, {}), - }, - }; - return this.ajax('/v1/sys/mfa/validate', 'POST', options); - }, - urlFor(endpoint) { if (!ENDPOINTS.includes(endpoint)) { throw new Error( diff --git a/ui/app/components/auth-form.js b/ui/app/components/auth-form.js index 8b1ed8aa8..5e7f8a7fc 100644 --- a/ui/app/components/auth-form.js +++ b/ui/app/components/auth-form.js @@ -18,13 +18,13 @@ const BACKENDS = supportedAuthBackends(); * * @example ```js * // All properties are passed in via query params. - * ``` + * ``` * - * @param {string} wrappedToken - The auth method that is currently selected in the dropdown. - * @param {object} cluster - The auth method that is currently selected in the dropdown. This corresponds to an Ember Model. - * @param {string} namespace- The currently active namespace. - * @param {string} selectedAuth - The auth method that is currently selected in the dropdown. - * @param {function} onSuccess - Fired on auth success + * @param wrappedToken=null {String} - The auth method that is currently selected in the dropdown. + * @param cluster=null {Object} - The auth method that is currently selected in the dropdown. This corresponds to an Ember Model. + * @param namespace=null {String} - The currently active namespace. + * @param redirectTo=null {String} - The name of the route to redirect to. + * @param selectedAuth=null {String} - The auth method that is currently selected in the dropdown. */ const DEFAULTS = { @@ -45,6 +45,7 @@ export default Component.extend(DEFAULTS, { selectedAuth: null, methods: null, cluster: null, + redirectTo: null, namespace: null, wrappedToken: null, // internal @@ -205,18 +206,54 @@ export default Component.extend(DEFAULTS, { showLoading: or('isLoading', 'authenticate.isRunning', 'fetchMethods.isRunning', 'unwrapToken.isRunning'), + handleError(e, prefixMessage = true) { + this.set('loading', false); + let errors; + if (e.errors) { + errors = e.errors.map((error) => { + if (error.detail) { + return error.detail; + } + return error; + }); + } else { + errors = [e]; + } + let message = prefixMessage ? 'Authentication failed: ' : ''; + this.set('error', `${message}${errors.join('.')}`); + }, + authenticate: task( waitFor(function* (backendType, data) { let clusterId = this.cluster.id; try { - this.delayAuthMessageReminder.perform(); - const authResponse = yield this.auth.authenticate({ clusterId, backend: backendType, data }); - this.onSuccess(authResponse, backendType, data); - } catch (e) { - this.set('loading', false); - if (!this.auth.mfaError) { - this.set('error', `Authentication failed: ${this.auth.handleError(e)}`); + if (backendType === 'okta') { + this.delayAuthMessageReminder.perform(); } + let authResponse = yield this.auth.authenticate({ clusterId, backend: backendType, data }); + + let { isRoot, namespace } = authResponse; + let transition; + let { redirectTo } = this; + if (redirectTo) { + // reset the value on the controller because it's bound here + this.set('redirectTo', ''); + // here we don't need the namespace because it will be encoded in redirectTo + transition = this.router.transitionTo(redirectTo); + } else { + transition = this.router.transitionTo('vault.cluster', { queryParams: { namespace } }); + } + // returning this w/then because if we keep it + // in the task, it will get cancelled when the component in un-rendered + yield transition.followRedirects().then(() => { + if (isRoot) { + this.flashMessages.warning( + 'You have logged in with a root token. As a security precaution, this root token will not be stored by your browser and you will need to re-authenticate after the window is closed or refreshed.' + ); + } + }); + } catch (e) { + this.handleError(e); } }) ), @@ -225,9 +262,9 @@ export default Component.extend(DEFAULTS, { if (Ember.testing) { this.showLoading = true; yield timeout(0); - } else { - yield timeout(5000); + return; } + yield timeout(5000); }), actions: { @@ -261,10 +298,11 @@ export default Component.extend(DEFAULTS, { return this.authenticate.unlinked().perform(backend.type, data); }, handleError(e) { - this.setProperties({ - loading: false, - error: e ? this.auth.handleError(e) : null, - }); + if (e) { + this.handleError(e, false); + } else { + this.set('error', null); + } }, }, }); diff --git a/ui/app/components/clients/current.js b/ui/app/components/clients/current.js index 9da877166..1e8a49080 100644 --- a/ui/app/components/clients/current.js +++ b/ui/app/components/clients/current.js @@ -15,6 +15,9 @@ export default class Current extends Component { return { name: namespace['label'], id: namespace['label'] }; }); + @tracked selectedAuthMethod = null; + @tracked authMethodOptions = []; + // Response client count data by namespace for current/partial month get byNamespaceCurrent() { return this.args.model.monthly?.byNamespace || []; @@ -26,7 +29,21 @@ export default class Current extends Component { } get hasAttributionData() { - return this.totalUsageCounts.clients !== 0 && this.totalClientsData.length !== 0; + return this.totalUsageCounts.clients !== 0 && !!this.totalClientsData && !this.selectedAuthMethod; + } + + get filteredActivity() { + const namespace = this.selectedNamespace; + const auth = this.selectedAuthMethod; + if (!namespace && !auth) { + return this.getActivityResponse; + } + if (!auth) { + return this.byNamespaceCurrent.find((ns) => ns.label === namespace); + } + return this.byNamespaceCurrent + .find((ns) => ns.label === namespace) + .mounts?.find((mount) => mount.label === auth); } get countsIncludeOlderData() { @@ -41,16 +58,13 @@ export default class Current extends Component { // top level TOTAL client counts for current/partial month get totalUsageCounts() { - return this.selectedNamespace - ? this.filterByNamespace(this.selectedNamespace) - : this.args.model.monthly?.total; + return this.selectedNamespace ? this.filteredActivity : this.args.model.monthly?.total; } // total client data for horizontal bar chart in attribution component get totalClientsData() { if (this.selectedNamespace) { - let filteredNamespace = this.filterByNamespace(this.selectedNamespace); - return filteredNamespace.mounts ? this.filterByNamespace(this.selectedNamespace).mounts : null; + return this.filteredActivity?.mounts || null; } else { return this.byNamespaceCurrent; } @@ -60,15 +74,26 @@ export default class Current extends Component { return this.args.model.monthly?.responseTimestamp; } - // HELPERS - filterByNamespace(namespace) { - return this.byNamespaceCurrent.find((ns) => ns.label === namespace); - } - // ACTIONS @action selectNamespace([value]) { // value comes in as [namespace0] this.selectedNamespace = value; + if (!value) { + // on clear, also make sure auth method is cleared + this.selectedAuthMethod = null; + } else { + // Side effect: set auth namespaces + const mounts = this.filteredActivity.mounts?.map((mount) => ({ + id: mount.label, + name: mount.label, + })); + this.authMethodOptions = mounts; + } + } + + @action + setAuthMethod([authMount]) { + this.selectedAuthMethod = authMount; } } diff --git a/ui/app/components/clients/history.js b/ui/app/components/clients/history.js index 89c34cf58..fdd163956 100644 --- a/ui/app/components/clients/history.js +++ b/ui/app/components/clients/history.js @@ -38,10 +38,15 @@ export default class History extends Component { years = Array.from({ length: 5 }, (item, i) => { return new Date().getFullYear() - i; }); + currentDate = new Date(); + currentYear = this.currentDate.getFullYear(); // integer of year + currentMonth = this.currentDate.getMonth(); // index of month @tracked isEditStartMonthOpen = false; @tracked startMonth = null; @tracked startYear = null; + @tracked allowedMonthMax = 12; + @tracked disabledYear = null; // FOR HISTORY COMPONENT // @@ -57,14 +62,19 @@ export default class History extends Component { // SEARCH SELECT @tracked selectedNamespace = null; - @tracked namespaceArray = this.getActivityResponse.byNamespace.map((namespace) => { - return { name: namespace['label'], id: namespace['label'] }; - }); + @tracked namespaceArray = this.getActivityResponse.byNamespace.map((namespace) => ({ + name: namespace.label, + id: namespace.label, + })); // TEMPLATE MESSAGING @tracked noActivityDate = ''; @tracked responseRangeDiffMessage = null; @tracked isLoadingQuery = false; + @tracked licenseStartIsCurrentMonth = this.args.model.activity?.isLicenseDateError || false; + + @tracked selectedAuthMethod = null; + @tracked authMethodOptions = []; get versionText() { return this.version.isEnterprise @@ -92,7 +102,7 @@ export default class History extends Component { } get hasAttributionData() { - return this.totalUsageCounts.clients !== 0 && this.totalClientsData.length !== 0; + return this.totalUsageCounts.clients !== 0 && !!this.totalClientsData && !this.selectedAuthMethod; } get startTimeDisplay() { @@ -113,6 +123,20 @@ export default class History extends Component { return `${this.arrayOfMonths[month]} ${year}`; } + get filteredActivity() { + const namespace = this.selectedNamespace; + const auth = this.selectedAuthMethod; + if (!namespace && !auth) { + return this.getActivityResponse; + } + if (!auth) { + return this.getActivityResponse.byNamespace.find((ns) => ns.label === namespace); + } + return this.getActivityResponse.byNamespace + .find((ns) => ns.label === namespace) + .mounts?.find((mount) => mount.label === auth); + } + get isDateRange() { return !isSameMonth( new Date(this.getActivityResponse.startTime), @@ -122,16 +146,13 @@ export default class History extends Component { // top level TOTAL client counts for given date range get totalUsageCounts() { - return this.selectedNamespace - ? this.filterByNamespace(this.selectedNamespace) - : this.getActivityResponse.total; + return this.selectedNamespace ? this.filteredActivity : this.getActivityResponse.total; } // total client data for horizontal bar chart in attribution component get totalClientsData() { if (this.selectedNamespace) { - let filteredNamespace = this.filterByNamespace(this.selectedNamespace); - return filteredNamespace.mounts ? this.filterByNamespace(this.selectedNamespace).mounts : null; + return this.filteredActivity?.mounts || null; } else { return this.getActivityResponse?.byNamespace; } @@ -157,6 +178,7 @@ export default class History extends Component { @action async handleClientActivityQuery(month, year, dateType) { + this.isEditStartMonthOpen = false; if (dateType === 'cancel') { return; } @@ -195,6 +217,7 @@ export default class History extends Component { this.storage().setItem(INPUTTED_START_DATE, this.startTimeFromResponse); } this.queriedActivityResponse = response; + this.licenseStartIsCurrentMonth = response.isLicenseDateError; // compare if the response startTime comes after the requested startTime. If true throw a warning. // only display if they selected a startTime if ( @@ -209,7 +232,6 @@ export default class History extends Component { this.responseRangeDiffMessage = null; } } catch (e) { - // TODO CMB surface API errors when user selects start date after end date return e; } finally { this.isLoadingQuery = false; @@ -225,22 +247,38 @@ export default class History extends Component { selectNamespace([value]) { // value comes in as [namespace0] this.selectedNamespace = value; + if (!value) { + // on clear, also make sure auth method is cleared + this.selectedAuthMethod = null; + } else { + // Side effect: set auth namespaces + const mounts = this.filteredActivity.mounts?.map((mount) => ({ + id: mount.label, + name: mount.label, + })); + this.authMethodOptions = mounts; + } + } + + @action + setAuthMethod([authMount]) { + this.selectedAuthMethod = authMount; } // FOR START DATE MODAL @action - selectStartMonth(month) { + selectStartMonth(month, event) { this.startMonth = month; + // disables months if in the future + this.disabledYear = this.months.indexOf(month) >= this.currentMonth ? this.currentYear : null; + event.close(); } @action - selectStartYear(year) { + selectStartYear(year, event) { this.startYear = year; - } - - // HELPERS // - filterByNamespace(namespace) { - return this.getActivityResponse.byNamespace.find((ns) => ns.label === namespace); + this.allowedMonthMax = year === this.currentYear ? this.currentMonth : 12; + event.close(); } storage() { diff --git a/ui/app/components/date-dropdown.js b/ui/app/components/date-dropdown.js index ba78465a9..c40b412f2 100644 --- a/ui/app/components/date-dropdown.js +++ b/ui/app/components/date-dropdown.js @@ -12,9 +12,15 @@ import { tracked } from '@glimmer/tracking'; * ``` * @param {function} handleDateSelection - is the action from the parent that the date picker triggers * @param {string} [name] - optional argument passed from date dropdown to parent function + * @param {string} [submitText] - optional argument to change submit button text */ - export default class DateDropdown extends Component { + currentDate = new Date(); + currentYear = this.currentDate.getFullYear(); // integer of year + currentMonth = this.currentDate.getMonth(); // index of month + + @tracked allowedMonthMax = 12; + @tracked disabledYear = null; @tracked startMonth = null; @tracked startYear = null; @@ -26,13 +32,18 @@ export default class DateDropdown extends Component { }); @action - selectStartMonth(month) { + selectStartMonth(month, event) { this.startMonth = month; + // disables months if in the future + this.disabledYear = this.months.indexOf(month) >= this.currentMonth ? this.currentYear : null; + event.close(); } @action - selectStartYear(year) { + selectStartYear(year, event) { this.startYear = year; + this.allowedMonthMax = year === this.currentYear ? this.currentMonth : 12; + event.close(); } @action diff --git a/ui/app/components/mfa-error.js b/ui/app/components/mfa-error.js deleted file mode 100644 index ed894a051..000000000 --- a/ui/app/components/mfa-error.js +++ /dev/null @@ -1,43 +0,0 @@ -import Component from '@glimmer/component'; -import { inject as service } from '@ember/service'; -import { action } from '@ember/object'; -import { TOTP_NOT_CONFIGURED } from 'vault/services/auth'; - -const TOTP_NA_MSG = - 'Multi-factor authentication is required, but you have not set it up. In order to do so, please contact your administrator.'; -const MFA_ERROR_MSG = - 'Multi-factor authentication is required, but failed. Go back and try again, or contact your administrator.'; - -export { TOTP_NA_MSG, MFA_ERROR_MSG }; - -/** - * @module MfaError - * MfaError components are used to display mfa errors - * - * @example - * ```js - * - * ``` - */ - -export default class MfaError extends Component { - @service auth; - - get isTotp() { - return this.auth.mfaErrors.includes(TOTP_NOT_CONFIGURED); - } - get title() { - return this.isTotp ? 'TOTP not set up' : 'Unauthorized'; - } - get description() { - return this.isTotp ? TOTP_NA_MSG : MFA_ERROR_MSG; - } - - @action - onClose() { - this.auth.set('mfaErrors', null); - if (this.args.onClose) { - this.args.onClose(); - } - } -} diff --git a/ui/app/components/mfa-form.js b/ui/app/components/mfa-form.js deleted file mode 100644 index 6ba69b320..000000000 --- a/ui/app/components/mfa-form.js +++ /dev/null @@ -1,89 +0,0 @@ -import Component from '@glimmer/component'; -import { inject as service } from '@ember/service'; -import { tracked } from '@glimmer/tracking'; -import { action, set } from '@ember/object'; -import { task, timeout } from 'ember-concurrency'; -import { numberToWord } from 'vault/helpers/number-to-word'; -/** - * @module MfaForm - * The MfaForm component is used to enter a passcode when mfa is required to login - * - * @example - * ```js - * - * ``` - * @param {string} clusterId - id of selected cluster - * @param {object} authData - data from initial auth request -- { mfa_requirement, backend, data } - * @param {function} onSuccess - fired when passcode passes validation - */ - -export default class MfaForm extends Component { - @service auth; - - @tracked passcode; - @tracked countdown; - @tracked errors; - - get constraints() { - return this.args.authData.mfa_requirement.mfa_constraints; - } - get multiConstraint() { - return this.constraints.length > 1; - } - get singleConstraintMultiMethod() { - return !this.isMultiConstraint && this.constraints[0].methods.length > 1; - } - get singlePasscode() { - return ( - !this.isMultiConstraint && - this.constraints[0].methods.length === 1 && - this.constraints[0].methods[0].uses_passcode - ); - } - get description() { - let base = 'Multi-factor authentication is enabled for your account.'; - if (this.singlePasscode) { - base += ' Enter your authentication code to log in.'; - } - if (this.singleConstraintMultiMethod) { - base += ' Select the MFA method you wish to use.'; - } - if (this.multiConstraint) { - const num = this.constraints.length; - base += ` ${numberToWord(num, true)} methods are required for successful authentication.`; - } - return base; - } - - @task *validate() { - try { - const response = yield this.auth.totpValidate({ - clusterId: this.args.clusterId, - ...this.args.authData, - }); - this.args.onSuccess(response); - } catch (error) { - this.errors = error.errors; - // TODO: update if specific error can be parsed for incorrect passcode - // this.newCodeDelay.perform(); - } - } - - @task *newCodeDelay() { - this.passcode = null; - this.countdown = 30; - while (this.countdown) { - yield timeout(1000); - this.countdown--; - } - } - - @action onSelect(constraint, id) { - set(constraint, 'selectedId', id); - set(constraint, 'selectedMethod', constraint.methods.findBy('id', id)); - } - @action submit(e) { - e.preventDefault(); - this.validate.perform(); - } -} diff --git a/ui/app/components/transit-edit.js b/ui/app/components/transit-edit.js index a24712bf1..33620345a 100644 --- a/ui/app/components/transit-edit.js +++ b/ui/app/components/transit-edit.js @@ -95,10 +95,10 @@ export default Component.extend(FocusOnInsertMixin, { handleAutoRotateChange(ttlObj) { if (ttlObj.enabled) { - set(this.key, 'autoRotateInterval', ttlObj.goSafeTimeString); + set(this.key, 'autoRotatePeriod', ttlObj.goSafeTimeString); this.set('autoRotateInvalid', ttlObj.seconds < 3600); } else { - set(this.key, 'autoRotateInterval', 0); + set(this.key, 'autoRotatePeriod', 0); } }, diff --git a/ui/app/controllers/vault/cluster/auth.js b/ui/app/controllers/vault/cluster/auth.js index 3e98db58e..103fff827 100644 --- a/ui/app/controllers/vault/cluster/auth.js +++ b/ui/app/controllers/vault/cluster/auth.js @@ -8,18 +8,13 @@ export default Controller.extend({ clusterController: controller('vault.cluster'), namespaceService: service('namespace'), featureFlagService: service('featureFlag'), - auth: service(), - router: service(), - - queryParams: [{ authMethod: 'with', oidcProvider: 'o' }], - namespaceQueryParam: alias('clusterController.namespaceQueryParam'), + queryParams: [{ authMethod: 'with', oidcProvider: 'o' }], wrappedToken: alias('vaultController.wrappedToken'), - redirectTo: alias('vaultController.redirectTo'), - managedNamespaceRoot: alias('featureFlagService.managedNamespaceRoot'), - authMethod: '', oidcProvider: '', + redirectTo: alias('vaultController.redirectTo'), + managedNamespaceRoot: alias('featureFlagService.managedNamespaceRoot'), get managedNamespaceChild() { let fullParam = this.namespaceQueryParam; @@ -46,39 +41,4 @@ export default Controller.extend({ this.namespaceService.setNamespace(value, true); this.set('namespaceQueryParam', value); }).restartable(), - - authSuccess({ isRoot, namespace }) { - let transition; - if (this.redirectTo) { - // here we don't need the namespace because it will be encoded in redirectTo - transition = this.router.transitionTo(this.redirectTo); - // reset the value on the controller because it's bound here - this.set('redirectTo', ''); - } else { - transition = this.router.transitionTo('vault.cluster', { queryParams: { namespace } }); - } - transition.followRedirects().then(() => { - if (isRoot) { - this.flashMessages.warning( - 'You have logged in with a root token. As a security precaution, this root token will not be stored by your browser and you will need to re-authenticate after the window is closed or refreshed.' - ); - } - }); - }, - - actions: { - onAuthResponse(authResponse, backend, data) { - const { mfa_requirement } = authResponse; - // mfa methods handled by the backend are validated immediately in the auth service - // if the user must choose between methods or enter passcodes further action is required - if (mfa_requirement) { - this.set('mfaAuthData', { mfa_requirement, backend, data }); - } else { - this.authSuccess(authResponse); - } - }, - onMfaSuccess(authResponse) { - this.authSuccess(authResponse); - }, - }, }); diff --git a/ui/app/helpers/format-duration.js b/ui/app/helpers/format-duration.js index 927e586e3..ac118098a 100644 --- a/ui/app/helpers/format-duration.js +++ b/ui/app/helpers/format-duration.js @@ -1,11 +1,15 @@ import { helper } from '@ember/component/helper'; import { formatDuration, intervalToDuration } from 'date-fns'; -export function duration([time]) { +export function duration([time], { removeZero = false }) { // intervalToDuration creates a durationObject that turns the seconds (ex 3600) to respective: // { years: 0, months: 0, days: 0, hours: 1, minutes: 0, seconds: 0 } // then formatDuration returns the filled in keys of the durationObject + if (removeZero && time === '0') { + return null; + } + // time must be in seconds let duration = Number.parseInt(time, 10); if (isNaN(duration)) { diff --git a/ui/app/helpers/number-to-word.js b/ui/app/helpers/number-to-word.js deleted file mode 100644 index 7369ecabd..000000000 --- a/ui/app/helpers/number-to-word.js +++ /dev/null @@ -1,22 +0,0 @@ -import { helper } from '@ember/component/helper'; - -export function numberToWord(number, capitalize) { - const word = - { - 0: 'zero', - 1: 'one', - 2: 'two', - 3: 'three', - 4: 'four', - 5: 'five', - 6: 'six', - 7: 'seven', - 8: 'eight', - 9: 'nine', - }[number] || number; - return capitalize && typeof word === 'string' ? `${word.charAt(0).toUpperCase()}${word.slice(1)}` : word; -} - -export default helper(function ([number], { capitalize }) { - return numberToWord(number, capitalize); -}); diff --git a/ui/app/models/transit-key.js b/ui/app/models/transit-key.js index 048646843..4c849a4d7 100644 --- a/ui/app/models/transit-key.js +++ b/ui/app/models/transit-key.js @@ -56,11 +56,11 @@ export default Model.extend({ fieldValue: 'id', readOnly: true, }), - autoRotateInterval: attr({ + autoRotatePeriod: attr({ defaultValue: '0', defaultShown: 'Key is not automatically rotated', editType: 'ttl', - label: 'Auto-rotation interval', + label: 'Auto-rotation period', }), deletionAllowed: attr('boolean'), derived: attr('boolean'), diff --git a/ui/app/routes/vault/cluster/clients/history.js b/ui/app/routes/vault/cluster/clients/history.js index 9fa30faa5..db7661bc1 100644 --- a/ui/app/routes/vault/cluster/clients/history.js +++ b/ui/app/routes/vault/cluster/clients/history.js @@ -8,10 +8,13 @@ export default class HistoryRoute extends Route { try { // on init ONLY make network request if we have a start time from the license // otherwise user needs to manually input - // TODO CMB what to return here? return start_time ? await this.store.queryRecord('clients/activity', { start_time }) : {}; } catch (e) { - return e; + // returns 400 when license start date is in the current month + if (e.httpStatus === 400) { + return { isLicenseDateError: true }; + } + throw e; } } diff --git a/ui/app/serializers/clients/config.js b/ui/app/serializers/clients/config.js index c39ac69f4..e1d0cdc3a 100644 --- a/ui/app/serializers/clients/config.js +++ b/ui/app/serializers/clients/config.js @@ -10,7 +10,7 @@ export default ApplicationSerializer.extend({ id: payload.id, data: { ...payload.data, - enabled: payload.data.enabled.includes('enable') ? 'On' : 'Off', + enabled: payload.data.enabled?.includes('enable') ? 'On' : 'Off', }, }; return this._super(store, primaryModelClass, normalizedPayload, id, requestType); diff --git a/ui/app/serializers/transit-key.js b/ui/app/serializers/transit-key.js index fec0f28f0..127f49fa3 100644 --- a/ui/app/serializers/transit-key.js +++ b/ui/app/serializers/transit-key.js @@ -50,12 +50,12 @@ export default RESTSerializer.extend({ const min_decryption_version = snapshot.attr('minDecryptionVersion'); const min_encryption_version = snapshot.attr('minEncryptionVersion'); const deletion_allowed = snapshot.attr('deletionAllowed'); - const auto_rotate_interval = snapshot.attr('autoRotateInterval'); + const auto_rotate_period = snapshot.attr('autoRotatePeriod'); return { min_decryption_version, min_encryption_version, deletion_allowed, - auto_rotate_interval, + auto_rotate_period, }; } else { snapshot.id = snapshot.attr('name'); diff --git a/ui/app/services/auth.js b/ui/app/services/auth.js index b25e50282..ee1fb727c 100644 --- a/ui/app/services/auth.js +++ b/ui/app/services/auth.js @@ -3,7 +3,6 @@ import { resolve, reject } from 'rsvp'; import { assign } from '@ember/polyfills'; import { isArray } from '@ember/array'; import { computed, get } from '@ember/object'; -import { capitalize } from '@ember/string'; import fetch from 'fetch'; import { getOwner } from '@ember/application'; @@ -15,10 +14,9 @@ import { task, timeout } from 'ember-concurrency'; const TOKEN_SEPARATOR = '☃'; const TOKEN_PREFIX = 'vault-'; const ROOT_PREFIX = '_root_'; -const TOTP_NOT_CONFIGURED = 'TOTP mfa required but not configured'; const BACKENDS = supportedAuthBackends(); -export { TOKEN_SEPARATOR, TOKEN_PREFIX, ROOT_PREFIX, TOTP_NOT_CONFIGURED }; +export { TOKEN_SEPARATOR, TOKEN_PREFIX, ROOT_PREFIX }; export default Service.extend({ permissions: service(), @@ -26,8 +24,6 @@ export default Service.extend({ IDLE_TIMEOUT: 3 * 60e3, expirationCalcTS: null, isRenewing: false, - mfaErrors: null, - init() { this._super(...arguments); this.checkForRootToken(); @@ -326,98 +322,16 @@ export default Service.extend({ }); }, - _parseMfaResponse(mfa_requirement) { - // mfa_requirement response comes back in a shape that is not easy to work with - // convert to array of objects and add necessary properties to satisfy the view - if (mfa_requirement) { - const { mfa_request_id, mfa_constraints } = mfa_requirement; - let requiresAction; // if multiple constraints or methods or passcode input is needed further action will be required - const constraints = []; - for (let key in mfa_constraints) { - const methods = mfa_constraints[key].any; - const isMulti = methods.length > 1; - if (isMulti || methods.findBy('uses_passcode')) { - requiresAction = true; - } - // friendly label for display in MfaForm - methods.forEach((m) => { - const typeFormatted = m.type === 'totp' ? m.type.toUpperCase() : capitalize(m.type); - m.label = `${typeFormatted} ${m.uses_passcode ? 'passcode' : 'push notification'}`; - }); - constraints.push({ - name: key, - methods, - selectedMethod: isMulti ? null : methods[0], - }); - } - - return { - mfa_requirement: { mfa_request_id, mfa_constraints: constraints }, - requiresAction, - }; - } - return {}; - }, - async authenticate(/*{clusterId, backend, data}*/) { const [options] = arguments; const adapter = this.clusterAdapter(); - let resp; - try { - resp = await adapter.authenticate(options); - } catch (e) { - // TODO: check for totp not configured mfa error before throwing - const errors = this.handleError(e); - // stubbing error - verify once API is finalized - if (errors.includes(TOTP_NOT_CONFIGURED)) { - this.set('mfaErrors', errors); - } - throw e; - } - - const { mfa_requirement, requiresAction } = this._parseMfaResponse(resp.auth?.mfa_requirement); - if (mfa_requirement) { - if (requiresAction) { - return { mfa_requirement }; - } - // silently make request to validate endpoint when passcode is not required - try { - resp = await adapter.mfaValidate(mfa_requirement); - } catch (e) { - // it's not clear in the auth-form component whether mfa validation is taking place for non-totp method - // since mfa errors display a screen rather than flash message handle separately - this.set('mfaErrors', this.handleError(e)); - throw e; - } - } - - return this.authSuccess(options, resp.auth || resp.data); - }, - - async totpValidate({ mfa_requirement, ...options }) { - const resp = await this.clusterAdapter().mfaValidate(mfa_requirement); - return this.authSuccess(options, resp.auth || resp.data); - }, - - async authSuccess(options, response) { - const authData = await this.persistAuthData(options, response, this.namespaceService.path); + let resp = await adapter.authenticate(options); + let authData = await this.persistAuthData(options, resp.auth || resp.data, this.namespaceService.path); await this.permissions.getPaths.perform(); return authData; }, - handleError(e) { - if (e.errors) { - return e.errors.map((error) => { - if (error.detail) { - return error.detail; - } - return error; - }); - } - return [e]; - }, - getAuthType() { if (!this.authData) return; return this.authData.backend.type; diff --git a/ui/app/styles/components/icon.scss b/ui/app/styles/components/icon.scss index c97c63ed6..bbb55ec20 100644 --- a/ui/app/styles/components/icon.scss +++ b/ui/app/styles/components/icon.scss @@ -51,7 +51,3 @@ margin-right: 4px; } } - -.icon-blue { - color: $blue; -} diff --git a/ui/app/styles/core/buttons.scss b/ui/app/styles/core/buttons.scss index a00ead578..24bb5c391 100644 --- a/ui/app/styles/core/buttons.scss +++ b/ui/app/styles/core/buttons.scss @@ -54,7 +54,7 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12); background-color: $color; color: $color-invert; - &:hover, + &:hover:not([disabled]), &.is-hovered { background-color: darken($color, 5%); border-color: darken($color, 5%); @@ -237,11 +237,3 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12); padding: $size-8; width: 100%; } - -.icon-button { - background: transparent; - padding: 0; - margin: 0; - border: none; - cursor: pointer; -} diff --git a/ui/app/styles/core/helpers.scss b/ui/app/styles/core/helpers.scss index 27cb0d244..73f31d248 100644 --- a/ui/app/styles/core/helpers.scss +++ b/ui/app/styles/core/helpers.scss @@ -19,9 +19,6 @@ .is-borderless { border: none !important; } -.is-box-shadowless { - box-shadow: none !important; -} .is-relative { position: relative; } @@ -191,9 +188,6 @@ .has-top-margin-xl { margin-top: $spacing-xl; } -.has-top-margin-xxl { - margin-top: $spacing-xxl; -} .has-border-bottom-light { border-radius: 0; border-bottom: 1px solid $grey-light; @@ -210,9 +204,7 @@ ul.bullet { .has-text-semibold { font-weight: $font-weight-semibold; } -.is-v-centered { - vertical-align: middle; -} + .has-text-grey-400 { color: $ui-gray-400; } diff --git a/ui/app/templates/components/clients/current.hbs b/ui/app/templates/components/clients/current.hbs index 9fc006746..324ca3f43 100644 --- a/ui/app/templates/components/clients/current.hbs +++ b/ui/app/templates/components/clients/current.hbs @@ -32,7 +32,20 @@ @onChange={{this.selectNamespace}} @placeholder={{"Filter by namespace"}} @displayInherit={{true}} + class="is-marginless" /> + {{#if this.selectedNamespace}} + + {{/if}} diff --git a/ui/app/templates/components/clients/history.hbs b/ui/app/templates/components/clients/history.hbs index ace2a53e8..f738eff12 100644 --- a/ui/app/templates/components/clients/history.hbs +++ b/ui/app/templates/components/clients/history.hbs @@ -14,13 +14,22 @@ Edit {{else}} - + {{/if}}

{{this.versionText.description}}

- {{#if (eq @model.config.queriesAvailable false)}} + {{#if this.licenseStartIsCurrentMonth}} + + + + {{else if (eq @model.config.queriesAvailable false)}} {{#if (eq @model.config.enabled "On")}} + {{/if}} + {{#if this.selectedNamespace}} + {{/if}} @@ -125,8 +147,10 @@ {{/if}} {{/if}} - {{else}} + {{else if (or (not @model.startTimeFromLicense) (not this.startTimeFromResponse))}} + {{else}} + {{/if}} {{/if}} @@ -155,11 +179,12 @@