diff --git a/api/client.go b/api/client.go index 87f8c537e9f1..9f84dd83d6fd 100644 --- a/api/client.go +++ b/api/client.go @@ -51,8 +51,6 @@ const ( EnvRateLimit = "VAULT_RATE_LIMIT" EnvHTTPProxy = "VAULT_HTTP_PROXY" HeaderIndex = "X-Vault-Index" - HeaderForward = "X-Vault-Forward" - HeaderInconsistent = "X-Vault-Inconsistent" ) // Deprecated values @@ -1397,7 +1395,7 @@ func ParseReplicationState(raw string, hmacKey []byte) (*logical.WALState, error // conjunction with RequireState. func ForwardInconsistent() RequestCallback { return func(req *Request) { - req.Headers.Set(HeaderInconsistent, "forward-active-node") + req.Headers.Set("X-Vault-Inconsistent", "forward-active-node") } } @@ -1406,7 +1404,7 @@ func ForwardInconsistent() RequestCallback { // This feature must be enabled in Vault's configuration. func ForwardAlways() RequestCallback { return func(req *Request) { - req.Headers.Set(HeaderForward, "active-node") + req.Headers.Set("X-Vault-Forward", "active-node") } } diff --git a/api/sys_mounts.go b/api/sys_mounts.go index 8a0c5b985470..1d68a1063fd0 100644 --- a/api/sys_mounts.go +++ b/api/sys_mounts.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "time" "github.com/mitchellh/mapstructure" ) @@ -66,31 +65,7 @@ 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, @@ -98,59 +73,16 @@ func (c *Sys) StartRemount(from, to string) (*MountMigrationOutput, error) { r := c.c.NewRequest("POST", "/v1/sys/remount") if err := r.SetJSONBody(body); err != nil { - return nil, err + return err } 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 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 + if err == nil { + defer resp.Body.Close() } - - return &result, err + return err } func (c *Sys) TuneMount(path string, config MountConfigInput) error { @@ -255,18 +187,3 @@ 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 ba478c4ffd50..a392966fa8ca 100644 --- a/builtin/credential/approle/path_login.go +++ b/builtin/credential/approle/path_login.go @@ -178,14 +178,11 @@ func (b *backend) pathLoginUpdate(ctx context.Context, req *logical.Request, dat } belongs, err := cidrutil.IPBelongsToCIDRBlocksSlice(req.Connection.RemoteAddr, entry.CIDRList) - if err != nil { - return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest - } - - if !belongs { + if !belongs || err != nil { return logical.ErrorResponse(fmt.Errorf( - "source address %q unauthorized through CIDR restrictions on the secret ID", + "source address %q unauthorized through CIDR restrictions on the secret ID: %w", 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 5b759393870a..42ae388a6d6c 100644 --- a/builtin/logical/ssh/path_config_ca.go +++ b/builtin/logical/ssh/path_config_ca.go @@ -10,7 +10,6 @@ import ( "crypto/rsa" "crypto/x509" "encoding/pem" - "errors" "fmt" "io" @@ -18,8 +17,6 @@ import ( "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" "golang.org/x/crypto/ssh" - - "github.com/mikesmitty/edkey" ) const ( @@ -360,9 +357,9 @@ func generateSSHKeyPair(randomSource io.Reader, keyType string, keyBits int) (st return "", "", err } - marshalled := edkey.MarshalED25519PrivateKey(privateSeed) - if marshalled == nil { - return "", "", errors.New("unable to marshal ed25519 private key") + marshalled, err := x509.MarshalPKCS8PrivateKey(privateSeed) + if err != nil { + return "", "", err } privateBlock = &pem.Block{ diff --git a/builtin/logical/ssh/path_config_ca_test.go b/builtin/logical/ssh/path_config_ca_test.go index 1a04d9dbeb6f..d346c5710c17 100644 --- a/builtin/logical/ssh/path_config_ca_test.go +++ b/builtin/logical/ssh/path_config_ca_test.go @@ -191,31 +191,17 @@ 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) } } @@ -249,24 +235,6 @@ 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 acc714d5fe6a..3cab23fc6809 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 period defined which has passed. This operation only happens +// auto rotate interval 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 period is 0, it should not + // If the policy's automatic rotation interval is 0, it should not // automatically rotate. - if p.AutoRotatePeriod == 0 { + if p.AutoRotateInterval == 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.AutoRotatePeriod)) { + if time.Now().After(latestKey.CreationTime.Add(p.AutoRotateInterval)) { 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 c49768e739e5..6ad7b2d7deb5 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_period": 24 * time.Hour, + "auto_rotate_interval": 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 period on one key to be one nanosecond + // Update auto rotate interval 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.AutoRotatePeriod = time.Nanosecond + p.AutoRotateInterval = 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 a3d72731b773..336643227579 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_period": { + "auto_rotate_interval": { 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 * } } - autoRotatePeriodRaw, ok, err := d.GetOkErr("auto_rotate_period") + autoRotateIntervalRaw, ok, err := d.GetOkErr("auto_rotate_interval") if err != nil { return nil, err } if ok { - autoRotatePeriod := time.Second * time.Duration(autoRotatePeriodRaw.(int)) + autoRotateInterval := time.Second * time.Duration(autoRotateIntervalRaw.(int)) // Provided value must be 0 to disable or at least an hour - 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 != 0 && autoRotateInterval < time.Hour { + return logical.ErrorResponse("auto rotate interval must be 0 to disable or at least an hour"), nil } - if autoRotatePeriod != p.AutoRotatePeriod { - p.AutoRotatePeriod = autoRotatePeriod + if autoRotateInterval != p.AutoRotateInterval { + p.AutoRotateInterval = autoRotateInterval persistNeeded = true } } diff --git a/builtin/logical/transit/path_config_test.go b/builtin/logical/transit/path_config_test.go index bc5843c65907..3888bc861a3b 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 { - initialAutoRotatePeriod interface{} - newAutoRotatePeriod interface{} - shouldError bool - expectedValue time.Duration + initialAutoRotateInterval interface{} + newAutoRotateInterval interface{} + shouldError bool + expectedValue time.Duration }{ "default (no value)": { - initialAutoRotatePeriod: "5h", - shouldError: false, - expectedValue: 5 * time.Hour, + initialAutoRotateInterval: "5h", + shouldError: false, + expectedValue: 5 * time.Hour, }, "0 (int)": { - initialAutoRotatePeriod: "5h", - newAutoRotatePeriod: 0, - shouldError: false, - expectedValue: 0, + initialAutoRotateInterval: "5h", + newAutoRotateInterval: 0, + shouldError: false, + expectedValue: 0, }, "0 (string)": { - initialAutoRotatePeriod: "5h", - newAutoRotatePeriod: 0, - shouldError: false, - expectedValue: 0, + initialAutoRotateInterval: "5h", + newAutoRotateInterval: 0, + shouldError: false, + expectedValue: 0, }, "5 seconds": { - newAutoRotatePeriod: "5s", - shouldError: true, + newAutoRotateInterval: "5s", + shouldError: true, }, "5 hours": { - newAutoRotatePeriod: "5h", - shouldError: false, - expectedValue: 5 * time.Hour, + newAutoRotateInterval: "5h", + shouldError: false, + expectedValue: 5 * time.Hour, }, "negative value": { - newAutoRotatePeriod: "-1800s", - shouldError: true, + newAutoRotateInterval: "-1800s", + shouldError: true, }, "invalid string": { - newAutoRotatePeriod: "this shouldn't work", - shouldError: true, + newAutoRotateInterval: "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_period": test.initialAutoRotatePeriod, + "auto_rotate_interval": test.initialAutoRotateInterval, }) resp, err := client.Logical().Write(fmt.Sprintf("transit/keys/%s/config", keyName), map[string]interface{}{ - "auto_rotate_period": test.newAutoRotatePeriod, + "auto_rotate_interval": test.newAutoRotateInterval, }) 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_period"].(json.Number) + gotRaw, ok := resp.Data["auto_rotate_interval"].(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_period returned, got: %d, want: %d", got, want) + t.Fatalf("incorrect auto_rotate_interval returned, got: %d, want: %d", got, want) } } }) diff --git a/builtin/logical/transit/path_keys.go b/builtin/logical/transit/path_keys.go index bcaf326c23dc..b05b28e8c8b2 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_period": { + "auto_rotate_interval": { 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) - autoRotatePeriod := time.Second * time.Duration(d.Get("auto_rotate_period").(int)) + autoRotateInterval := time.Second * time.Duration(d.Get("auto_rotate_interval").(int)) - 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 != 0 && autoRotateInterval < time.Hour { + return logical.ErrorResponse("auto rotate interval 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, - AutoRotatePeriod: autoRotatePeriod, + AutoRotateInterval: autoRotateInterval, } 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_period": int64(p.AutoRotatePeriod.Seconds()), + "auto_rotate_interval": int64(p.AutoRotateInterval.Seconds()), }, } diff --git a/builtin/logical/transit/path_keys_test.go b/builtin/logical/transit/path_keys_test.go index 04c1d8da092d..c74b580d19d2 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 { - autoRotatePeriod interface{} - shouldError bool - expectedValue time.Duration + autoRotateInterval interface{} + shouldError bool + expectedValue time.Duration }{ "default (no value)": { shouldError: false, }, "0 (int)": { - autoRotatePeriod: 0, - shouldError: false, - expectedValue: 0, + autoRotateInterval: 0, + shouldError: false, + expectedValue: 0, }, "0 (string)": { - autoRotatePeriod: "0", - shouldError: false, - expectedValue: 0, + autoRotateInterval: "0", + shouldError: false, + expectedValue: 0, }, "5 seconds": { - autoRotatePeriod: "5s", - shouldError: true, + autoRotateInterval: "5s", + shouldError: true, }, "5 hours": { - autoRotatePeriod: "5h", - shouldError: false, - expectedValue: 5 * time.Hour, + autoRotateInterval: "5h", + shouldError: false, + expectedValue: 5 * time.Hour, }, "negative value": { - autoRotatePeriod: "-1800s", - shouldError: true, + autoRotateInterval: "-1800s", + shouldError: true, }, "invalid string": { - autoRotatePeriod: "this shouldn't work", - shouldError: true, + autoRotateInterval: "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_period": test.autoRotatePeriod, + "auto_rotate_interval": test.autoRotateInterval, }) 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_period"].(json.Number) + gotRaw, ok := resp.Data["auto_rotate_interval"].(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_period returned, got: %d, want: %d", got, want) + t.Fatalf("incorrect auto_rotate_interval returned, got: %d, want: %d", got, want) } } }) diff --git a/changelog/14049.txt b/changelog/14049.txt new file mode 100644 index 000000000000..93af683bbdc3 --- /dev/null +++ b/changelog/14049.txt @@ -0,0 +1,3 @@ +```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 deleted file mode 100644 index bd24019ada4c..000000000000 --- a/changelog/14067.txt +++ /dev/null @@ -1,3 +0,0 @@ -```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 deleted file mode 100644 index f17138c0557f..000000000000 --- a/changelog/14107.txt +++ /dev/null @@ -1,3 +0,0 @@ -```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 ff33310476f2..a04ec090b377 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 works within or across namespaces, both source and destination paths - can be prefixed with a namespace heirarchy relative to the current namespace. + This command only works within a namespace; it cannot be used to move engines + to different namespaces. WARNING! Moving an existing secrets engine will revoke any leases from the old engine. @@ -39,11 +39,6 @@ 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) @@ -89,12 +84,11 @@ func (c *SecretsMoveCommand) Run(args []string) int { return 2 } - remountResp, err := client.Sys().StartRemount(source, destination) - if err != nil { + if err := client.Sys().Remount(source, destination); 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! Started moving secrets engine %s to %s, with migration ID %s", source, destination, remountResp.MigrationID)) + c.UI.Output(fmt.Sprintf("Success! Moved secrets engine %s to: %s", source, destination)) return 0 } diff --git a/command/secrets_move_test.go b/command/secrets_move_test.go index bca2a530fc16..1af52d131db6 100644 --- a/command/secrets_move_test.go +++ b/command/secrets_move_test.go @@ -3,7 +3,6 @@ package command import ( "strings" "testing" - "time" "github.com/mitchellh/cli" ) @@ -92,16 +91,12 @@ func TestSecretsMoveCommand_Run(t *testing.T) { t.Errorf("expected %d to be %d", code, exp) } - expected := "Success! Started moving secrets engine secret/ to generic/" + expected := "Success! Moved 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 14dae7da8215..73b048527843 100644 --- a/go.mod +++ b/go.mod @@ -306,7 +306,6 @@ 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 dc98c60a2e64..654db472105b 100644 --- a/go.sum +++ b/go.sum @@ -1160,8 +1160,6 @@ github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7 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 96d6043fff66..1b59495cab84 100644 --- a/helper/namespace/namespace.go +++ b/helper/namespace/namespace.go @@ -125,20 +125,3 @@ 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 71c454a9e73e..5c5bfabb5f89 100644 --- a/http/sys_mount_test.go +++ b/http/sys_mount_test.go @@ -2,10 +2,8 @@ package http import ( "encoding/json" - "fmt" "reflect" "testing" - "time" "github.com/go-test/deep" @@ -376,24 +374,8 @@ func TestSysRemount(t *testing.T) { "from": "foo", "to": "bar", }) - 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) + testResponseStatus(t, resp, 204) - 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 a424ca9e05b4..71bfcac8417b 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 - AutoRotatePeriod time.Duration + AutoRotateInterval 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, - AutoRotatePeriod: req.AutoRotatePeriod, + AutoRotateInterval: req.AutoRotateInterval, } if req.Derived { diff --git a/sdk/helper/keysutil/policy.go b/sdk/helper/keysutil/policy.go index 59afa99fde23..d4b82ab82e29 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"` - // AutoRotatePeriod defines how frequently the key should automatically + // AutoRotateInterval defines how frequently the key should automatically // rotate. Setting this to zero disables automatic rotation for the key. - AutoRotatePeriod time.Duration `json:"auto_rotate_period"` + AutoRotateInterval time.Duration `json:"auto_rotate_interval"` // 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 4b52350b1992..f8600442640d 100644 --- a/ui/app/adapters/cluster.js +++ b/ui/app/adapters/cluster.js @@ -126,6 +126,19 @@ 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 5e7f8a7fc3c1..8b1ed8aa837a 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 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. + * @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 */ const DEFAULTS = { @@ -45,7 +45,6 @@ export default Component.extend(DEFAULTS, { selectedAuth: null, methods: null, cluster: null, - redirectTo: null, namespace: null, wrappedToken: null, // internal @@ -206,54 +205,18 @@ 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 { - 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.' - ); - } - }); + this.delayAuthMessageReminder.perform(); + const authResponse = yield this.auth.authenticate({ clusterId, backend: backendType, data }); + this.onSuccess(authResponse, backendType, data); } catch (e) { - this.handleError(e); + this.set('loading', false); + if (!this.auth.mfaError) { + this.set('error', `Authentication failed: ${this.auth.handleError(e)}`); + } } }) ), @@ -262,9 +225,9 @@ export default Component.extend(DEFAULTS, { if (Ember.testing) { this.showLoading = true; yield timeout(0); - return; + } else { + yield timeout(5000); } - yield timeout(5000); }), actions: { @@ -298,11 +261,10 @@ export default Component.extend(DEFAULTS, { return this.authenticate.unlinked().perform(backend.type, data); }, handleError(e) { - if (e) { - this.handleError(e, false); - } else { - this.set('error', null); - } + this.setProperties({ + loading: false, + error: e ? this.auth.handleError(e) : null, + }); }, }, }); diff --git a/ui/app/components/clients/current.js b/ui/app/components/clients/current.js index 1e8a490803c6..9da877166ef0 100644 --- a/ui/app/components/clients/current.js +++ b/ui/app/components/clients/current.js @@ -15,9 +15,6 @@ 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 || []; @@ -29,21 +26,7 @@ export default class Current extends Component { } get hasAttributionData() { - 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); + return this.totalUsageCounts.clients !== 0 && this.totalClientsData.length !== 0; } get countsIncludeOlderData() { @@ -58,13 +41,16 @@ export default class Current extends Component { // top level TOTAL client counts for current/partial month get totalUsageCounts() { - return this.selectedNamespace ? this.filteredActivity : this.args.model.monthly?.total; + return this.selectedNamespace + ? this.filterByNamespace(this.selectedNamespace) + : this.args.model.monthly?.total; } // total client data for horizontal bar chart in attribution component get totalClientsData() { if (this.selectedNamespace) { - return this.filteredActivity?.mounts || null; + let filteredNamespace = this.filterByNamespace(this.selectedNamespace); + return filteredNamespace.mounts ? this.filterByNamespace(this.selectedNamespace).mounts : null; } else { return this.byNamespaceCurrent; } @@ -74,26 +60,15 @@ 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 fdd16395642d..89c34cf58615 100644 --- a/ui/app/components/clients/history.js +++ b/ui/app/components/clients/history.js @@ -38,15 +38,10 @@ 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 // @@ -62,19 +57,14 @@ export default class History extends Component { // SEARCH SELECT @tracked selectedNamespace = null; - @tracked namespaceArray = this.getActivityResponse.byNamespace.map((namespace) => ({ - name: namespace.label, - id: namespace.label, - })); + @tracked namespaceArray = this.getActivityResponse.byNamespace.map((namespace) => { + return { 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 @@ -102,7 +92,7 @@ export default class History extends Component { } get hasAttributionData() { - return this.totalUsageCounts.clients !== 0 && !!this.totalClientsData && !this.selectedAuthMethod; + return this.totalUsageCounts.clients !== 0 && this.totalClientsData.length !== 0; } get startTimeDisplay() { @@ -123,20 +113,6 @@ 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), @@ -146,13 +122,16 @@ export default class History extends Component { // top level TOTAL client counts for given date range get totalUsageCounts() { - return this.selectedNamespace ? this.filteredActivity : this.getActivityResponse.total; + return this.selectedNamespace + ? this.filterByNamespace(this.selectedNamespace) + : this.getActivityResponse.total; } // total client data for horizontal bar chart in attribution component get totalClientsData() { if (this.selectedNamespace) { - return this.filteredActivity?.mounts || null; + let filteredNamespace = this.filterByNamespace(this.selectedNamespace); + return filteredNamespace.mounts ? this.filterByNamespace(this.selectedNamespace).mounts : null; } else { return this.getActivityResponse?.byNamespace; } @@ -178,7 +157,6 @@ export default class History extends Component { @action async handleClientActivityQuery(month, year, dateType) { - this.isEditStartMonthOpen = false; if (dateType === 'cancel') { return; } @@ -217,7 +195,6 @@ 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 ( @@ -232,6 +209,7 @@ 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; @@ -247,38 +225,22 @@ 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, event) { + selectStartMonth(month) { 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, event) { + selectStartYear(year) { this.startYear = year; - this.allowedMonthMax = year === this.currentYear ? this.currentMonth : 12; - event.close(); + } + + // HELPERS // + filterByNamespace(namespace) { + return this.getActivityResponse.byNamespace.find((ns) => ns.label === namespace); } storage() { diff --git a/ui/app/components/date-dropdown.js b/ui/app/components/date-dropdown.js index c40b412f225f..ba78465a91a2 100644 --- a/ui/app/components/date-dropdown.js +++ b/ui/app/components/date-dropdown.js @@ -12,15 +12,9 @@ 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; +export default class DateDropdown extends Component { @tracked startMonth = null; @tracked startYear = null; @@ -32,18 +26,13 @@ export default class DateDropdown extends Component { }); @action - selectStartMonth(month, event) { + selectStartMonth(month) { 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, event) { + selectStartYear(year) { 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 new file mode 100644 index 000000000000..ed894a051a4c --- /dev/null +++ b/ui/app/components/mfa-error.js @@ -0,0 +1,43 @@ +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 new file mode 100644 index 000000000000..6ba69b32035e --- /dev/null +++ b/ui/app/components/mfa-form.js @@ -0,0 +1,89 @@ +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 33620345a403..a24712bf1ee1 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, 'autoRotatePeriod', ttlObj.goSafeTimeString); + set(this.key, 'autoRotateInterval', ttlObj.goSafeTimeString); this.set('autoRotateInvalid', ttlObj.seconds < 3600); } else { - set(this.key, 'autoRotatePeriod', 0); + set(this.key, 'autoRotateInterval', 0); } }, diff --git a/ui/app/controllers/vault/cluster/auth.js b/ui/app/controllers/vault/cluster/auth.js index 103fff827554..3e98db58ee73 100644 --- a/ui/app/controllers/vault/cluster/auth.js +++ b/ui/app/controllers/vault/cluster/auth.js @@ -8,14 +8,19 @@ export default Controller.extend({ clusterController: controller('vault.cluster'), namespaceService: service('namespace'), featureFlagService: service('featureFlag'), - namespaceQueryParam: alias('clusterController.namespaceQueryParam'), + auth: service(), + router: service(), + queryParams: [{ authMethod: 'with', oidcProvider: 'o' }], + + namespaceQueryParam: alias('clusterController.namespaceQueryParam'), wrappedToken: alias('vaultController.wrappedToken'), - authMethod: '', - oidcProvider: '', redirectTo: alias('vaultController.redirectTo'), managedNamespaceRoot: alias('featureFlagService.managedNamespaceRoot'), + authMethod: '', + oidcProvider: '', + get managedNamespaceChild() { let fullParam = this.namespaceQueryParam; let split = fullParam.split('/'); @@ -41,4 +46,39 @@ 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 8d6c45c39b98..515065bd1100 100644 --- a/ui/app/helpers/format-duration.js +++ b/ui/app/helpers/format-duration.js @@ -2,15 +2,11 @@ import { helper } from '@ember/component/helper'; import { assert } from '@ember/debug'; import { formatDuration, intervalToDuration } from 'date-fns'; -export function duration([time], { removeZero = false }) { +export function duration([time]) { // 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); assert('could not parse time', !isNaN(duration)); diff --git a/ui/app/helpers/number-to-word.js b/ui/app/helpers/number-to-word.js new file mode 100644 index 000000000000..7369ecabd7ad --- /dev/null +++ b/ui/app/helpers/number-to-word.js @@ -0,0 +1,22 @@ +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 4c849a4d7746..048646843c22 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, }), - autoRotatePeriod: attr({ + autoRotateInterval: attr({ defaultValue: '0', defaultShown: 'Key is not automatically rotated', editType: 'ttl', - label: 'Auto-rotation period', + label: 'Auto-rotation interval', }), 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 db7661bc11a0..9fa30faa5e0d 100644 --- a/ui/app/routes/vault/cluster/clients/history.js +++ b/ui/app/routes/vault/cluster/clients/history.js @@ -8,13 +8,10 @@ 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) { - // returns 400 when license start date is in the current month - if (e.httpStatus === 400) { - return { isLicenseDateError: true }; - } - throw e; + return e; } } diff --git a/ui/app/serializers/clients/config.js b/ui/app/serializers/clients/config.js index e1d0cdc3a044..c39ac69f4090 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 127f49fa33cc..fec0f28f0079 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_period = snapshot.attr('autoRotatePeriod'); + const auto_rotate_interval = snapshot.attr('autoRotateInterval'); return { min_decryption_version, min_encryption_version, deletion_allowed, - auto_rotate_period, + auto_rotate_interval, }; } else { snapshot.id = snapshot.attr('name'); diff --git a/ui/app/services/auth.js b/ui/app/services/auth.js index ee1fb727cf71..b25e502828c8 100644 --- a/ui/app/services/auth.js +++ b/ui/app/services/auth.js @@ -3,6 +3,7 @@ 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'; @@ -14,9 +15,10 @@ 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 }; +export { TOKEN_SEPARATOR, TOKEN_PREFIX, ROOT_PREFIX, TOTP_NOT_CONFIGURED }; export default Service.extend({ permissions: service(), @@ -24,6 +26,8 @@ export default Service.extend({ IDLE_TIMEOUT: 3 * 60e3, expirationCalcTS: null, isRenewing: false, + mfaErrors: null, + init() { this._super(...arguments); this.checkForRootToken(); @@ -322,16 +326,98 @@ 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; + } - let resp = await adapter.authenticate(options); - let authData = await this.persistAuthData(options, resp.auth || resp.data, this.namespaceService.path); + 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); 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 bbb55ec20560..c97c63ed6795 100644 --- a/ui/app/styles/components/icon.scss +++ b/ui/app/styles/components/icon.scss @@ -51,3 +51,7 @@ margin-right: 4px; } } + +.icon-blue { + color: $blue; +} diff --git a/ui/app/styles/core/buttons.scss b/ui/app/styles/core/buttons.scss index 24bb5c391307..a00ead578787 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:not([disabled]), + &:hover, &.is-hovered { background-color: darken($color, 5%); border-color: darken($color, 5%); @@ -237,3 +237,11 @@ $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 73f31d248fcb..27cb0d2440fa 100644 --- a/ui/app/styles/core/helpers.scss +++ b/ui/app/styles/core/helpers.scss @@ -19,6 +19,9 @@ .is-borderless { border: none !important; } +.is-box-shadowless { + box-shadow: none !important; +} .is-relative { position: relative; } @@ -188,6 +191,9 @@ .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; @@ -204,7 +210,9 @@ 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 324ca3f43fb6..9fc00674681d 100644 --- a/ui/app/templates/components/clients/current.hbs +++ b/ui/app/templates/components/clients/current.hbs @@ -32,20 +32,7 @@ @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 f738eff12efe..ace2a53e8295 100644 --- a/ui/app/templates/components/clients/history.hbs +++ b/ui/app/templates/components/clients/history.hbs @@ -14,22 +14,13 @@ Edit {{else}} - + {{/if}}

{{this.versionText.description}}

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