This reverts commit 5f17953b5980e6438215d5cb62c8575d16c63193.
This commit is contained in:
parent
e4aab1b0cc
commit
b936db8332
|
@ -51,6 +51,8 @@ const (
|
||||||
EnvRateLimit = "VAULT_RATE_LIMIT"
|
EnvRateLimit = "VAULT_RATE_LIMIT"
|
||||||
EnvHTTPProxy = "VAULT_HTTP_PROXY"
|
EnvHTTPProxy = "VAULT_HTTP_PROXY"
|
||||||
HeaderIndex = "X-Vault-Index"
|
HeaderIndex = "X-Vault-Index"
|
||||||
|
HeaderForward = "X-Vault-Forward"
|
||||||
|
HeaderInconsistent = "X-Vault-Inconsistent"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Deprecated values
|
// Deprecated values
|
||||||
|
@ -1395,7 +1397,7 @@ func ParseReplicationState(raw string, hmacKey []byte) (*logical.WALState, error
|
||||||
// conjunction with RequireState.
|
// conjunction with RequireState.
|
||||||
func ForwardInconsistent() RequestCallback {
|
func ForwardInconsistent() RequestCallback {
|
||||||
return func(req *Request) {
|
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.
|
// This feature must be enabled in Vault's configuration.
|
||||||
func ForwardAlways() RequestCallback {
|
func ForwardAlways() RequestCallback {
|
||||||
return func(req *Request) {
|
return func(req *Request) {
|
||||||
req.Headers.Set("X-Vault-Forward", "active-node")
|
req.Headers.Set(HeaderForward, "active-node")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
)
|
)
|
||||||
|
@ -65,7 +66,31 @@ func (c *Sys) Unmount(path string) error {
|
||||||
return err
|
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 {
|
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{}{
|
body := map[string]interface{}{
|
||||||
"from": from,
|
"from": from,
|
||||||
"to": to,
|
"to": to,
|
||||||
|
@ -73,16 +98,59 @@ func (c *Sys) Remount(from, to string) error {
|
||||||
|
|
||||||
r := c.c.NewRequest("POST", "/v1/sys/remount")
|
r := c.c.NewRequest("POST", "/v1/sys/remount")
|
||||||
if err := r.SetJSONBody(body); err != nil {
|
if err := r.SetJSONBody(body); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||||
defer cancelFunc()
|
defer cancelFunc()
|
||||||
resp, err := c.c.RawRequestWithContext(ctx, r)
|
resp, err := c.c.RawRequestWithContext(ctx, r)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
defer resp.Body.Close()
|
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 {
|
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.
|
// Deprecated: This field will always be blank for newer server responses.
|
||||||
PluginName string `json:"plugin_name,omitempty" mapstructure:"plugin_name"`
|
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"`
|
||||||
|
}
|
||||||
|
|
|
@ -178,11 +178,14 @@ func (b *backend) pathLoginUpdate(ctx context.Context, req *logical.Request, dat
|
||||||
}
|
}
|
||||||
|
|
||||||
belongs, err := cidrutil.IPBelongsToCIDRBlocksSlice(req.Connection.RemoteAddr, entry.CIDRList)
|
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(
|
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,
|
req.Connection.RemoteAddr,
|
||||||
err,
|
|
||||||
).Error()), nil
|
).Error()), nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
@ -17,6 +18,8 @@ import (
|
||||||
"github.com/hashicorp/vault/sdk/framework"
|
"github.com/hashicorp/vault/sdk/framework"
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
|
|
||||||
|
"github.com/mikesmitty/edkey"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -357,9 +360,9 @@ func generateSSHKeyPair(randomSource io.Reader, keyType string, keyBits int) (st
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
marshalled, err := x509.MarshalPKCS8PrivateKey(privateSeed)
|
marshalled := edkey.MarshalED25519PrivateKey(privateSeed)
|
||||||
if err != nil {
|
if marshalled == nil {
|
||||||
return "", "", err
|
return "", "", errors.New("unable to marshal ed25519 private key")
|
||||||
}
|
}
|
||||||
|
|
||||||
privateBlock = &pem.Block{
|
privateBlock = &pem.Block{
|
||||||
|
|
|
@ -191,17 +191,31 @@ func createDeleteHelper(t *testing.T, b logical.Backend, config *logical.Backend
|
||||||
}
|
}
|
||||||
resp, err := b.HandleRequest(context.Background(), caReq)
|
resp, err := b.HandleRequest(context.Background(), caReq)
|
||||||
if err != nil || (resp != nil && resp.IsError()) {
|
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)) {
|
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"])
|
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
|
// Delete the configured keys
|
||||||
caReq.Operation = logical.DeleteOperation
|
caReq.Operation = logical.DeleteOperation
|
||||||
resp, err = b.HandleRequest(context.Background(), caReq)
|
resp, err = b.HandleRequest(context.Background(), caReq)
|
||||||
if err != nil || (resp != nil && resp.IsError()) {
|
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},
|
{"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 {
|
for index, scenario := range cases {
|
||||||
createDeleteHelper(t, b, config, index, scenario.keyType, scenario.keyBits)
|
createDeleteHelper(t, b, config, index, scenario.keyType, scenario.keyBits)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
// 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.
|
// on primary nodes and performance secondary nodes which have a local mount.
|
||||||
func (b *backend) autoRotateKeys(ctx context.Context, req *logical.Request) error {
|
func (b *backend) autoRotateKeys(ctx context.Context, req *logical.Request) error {
|
||||||
// Only check for autorotation once an hour to avoid unnecessarily iterating
|
// 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()
|
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.
|
// automatically rotate.
|
||||||
if p.AutoRotateInterval == 0 {
|
if p.AutoRotatePeriod == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the latest version of the policy and determine if it is time to rotate.
|
// Retrieve the latest version of the policy and determine if it is time to rotate.
|
||||||
latestKey := p.Keys[strconv.Itoa(p.LatestVersion)]
|
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() {
|
if b.Logger().IsDebug() {
|
||||||
b.Logger().Debug("automatically rotating key", "key", key)
|
b.Logger().Debug("automatically rotating key", "key", key)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1607,7 +1607,7 @@ func TestTransit_AutoRotateKeys(t *testing.T) {
|
||||||
Operation: logical.UpdateOperation,
|
Operation: logical.UpdateOperation,
|
||||||
Path: "keys/test2",
|
Path: "keys/test2",
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"auto_rotate_interval": 24 * time.Hour,
|
"auto_rotate_period": 24 * time.Hour,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
resp, err = b.HandleRequest(context.Background(), req)
|
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)
|
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{
|
p, _, err := b.GetPolicy(context.Background(), keysutil.PolicyRequest{
|
||||||
Storage: storage,
|
Storage: storage,
|
||||||
Name: "test2",
|
Name: "test2",
|
||||||
|
@ -1662,7 +1662,7 @@ func TestTransit_AutoRotateKeys(t *testing.T) {
|
||||||
if p == nil {
|
if p == nil {
|
||||||
t.Fatal("expected non-nil policy")
|
t.Fatal("expected non-nil policy")
|
||||||
}
|
}
|
||||||
p.AutoRotateInterval = time.Nanosecond
|
p.AutoRotatePeriod = time.Nanosecond
|
||||||
err = p.Persist(context.Background(), storage)
|
err = p.Persist(context.Background(), storage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
|
@ -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.`,
|
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,
|
Type: framework.TypeDurationSecond,
|
||||||
Description: `Amount of time the key should live before
|
Description: `Amount of time the key should live before
|
||||||
being automatically rotated. A value of 0
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if ok {
|
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
|
// Provided value must be 0 to disable or at least an hour
|
||||||
if autoRotateInterval != 0 && autoRotateInterval < time.Hour {
|
if autoRotatePeriod != 0 && autoRotatePeriod < time.Hour {
|
||||||
return logical.ErrorResponse("auto rotate interval must be 0 to disable or at least an hour"), nil
|
return logical.ErrorResponse("auto rotate period must be 0 to disable or at least an hour"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if autoRotateInterval != p.AutoRotateInterval {
|
if autoRotatePeriod != p.AutoRotatePeriod {
|
||||||
p.AutoRotateInterval = autoRotateInterval
|
p.AutoRotatePeriod = autoRotatePeriod
|
||||||
persistNeeded = true
|
persistNeeded = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -294,43 +294,43 @@ func TestTransit_ConfigSettings(t *testing.T) {
|
||||||
|
|
||||||
func TestTransit_UpdateKeyConfigWithAutorotation(t *testing.T) {
|
func TestTransit_UpdateKeyConfigWithAutorotation(t *testing.T) {
|
||||||
tests := map[string]struct {
|
tests := map[string]struct {
|
||||||
initialAutoRotateInterval interface{}
|
initialAutoRotatePeriod interface{}
|
||||||
newAutoRotateInterval interface{}
|
newAutoRotatePeriod interface{}
|
||||||
shouldError bool
|
shouldError bool
|
||||||
expectedValue time.Duration
|
expectedValue time.Duration
|
||||||
}{
|
}{
|
||||||
"default (no value)": {
|
"default (no value)": {
|
||||||
initialAutoRotateInterval: "5h",
|
initialAutoRotatePeriod: "5h",
|
||||||
shouldError: false,
|
shouldError: false,
|
||||||
expectedValue: 5 * time.Hour,
|
expectedValue: 5 * time.Hour,
|
||||||
},
|
},
|
||||||
"0 (int)": {
|
"0 (int)": {
|
||||||
initialAutoRotateInterval: "5h",
|
initialAutoRotatePeriod: "5h",
|
||||||
newAutoRotateInterval: 0,
|
newAutoRotatePeriod: 0,
|
||||||
shouldError: false,
|
shouldError: false,
|
||||||
expectedValue: 0,
|
expectedValue: 0,
|
||||||
},
|
},
|
||||||
"0 (string)": {
|
"0 (string)": {
|
||||||
initialAutoRotateInterval: "5h",
|
initialAutoRotatePeriod: "5h",
|
||||||
newAutoRotateInterval: 0,
|
newAutoRotatePeriod: 0,
|
||||||
shouldError: false,
|
shouldError: false,
|
||||||
expectedValue: 0,
|
expectedValue: 0,
|
||||||
},
|
},
|
||||||
"5 seconds": {
|
"5 seconds": {
|
||||||
newAutoRotateInterval: "5s",
|
newAutoRotatePeriod: "5s",
|
||||||
shouldError: true,
|
shouldError: true,
|
||||||
},
|
},
|
||||||
"5 hours": {
|
"5 hours": {
|
||||||
newAutoRotateInterval: "5h",
|
newAutoRotatePeriod: "5h",
|
||||||
shouldError: false,
|
shouldError: false,
|
||||||
expectedValue: 5 * time.Hour,
|
expectedValue: 5 * time.Hour,
|
||||||
},
|
},
|
||||||
"negative value": {
|
"negative value": {
|
||||||
newAutoRotateInterval: "-1800s",
|
newAutoRotatePeriod: "-1800s",
|
||||||
shouldError: true,
|
shouldError: true,
|
||||||
},
|
},
|
||||||
"invalid string": {
|
"invalid string": {
|
||||||
newAutoRotateInterval: "this shouldn't work",
|
newAutoRotatePeriod: "this shouldn't work",
|
||||||
shouldError: true,
|
shouldError: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -364,11 +364,11 @@ func TestTransit_UpdateKeyConfigWithAutorotation(t *testing.T) {
|
||||||
keyName := hex.EncodeToString(keyNameBytes)
|
keyName := hex.EncodeToString(keyNameBytes)
|
||||||
|
|
||||||
_, err = client.Logical().Write(fmt.Sprintf("transit/keys/%s", keyName), map[string]interface{}{
|
_, 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{}{
|
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 {
|
switch {
|
||||||
case test.shouldError && err == nil:
|
case test.shouldError && err == nil:
|
||||||
|
@ -385,7 +385,7 @@ func TestTransit_UpdateKeyConfigWithAutorotation(t *testing.T) {
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
t.Fatal("expected non-nil response")
|
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 {
|
if !ok {
|
||||||
t.Fatal("returned value is of unexpected type")
|
t.Fatal("returned value is of unexpected type")
|
||||||
}
|
}
|
||||||
|
@ -395,7 +395,7 @@ func TestTransit_UpdateKeyConfigWithAutorotation(t *testing.T) {
|
||||||
}
|
}
|
||||||
want := int64(test.expectedValue.Seconds())
|
want := int64(test.expectedValue.Seconds())
|
||||||
if got != want {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -95,7 +95,7 @@ if the key type supports public keys, this will
|
||||||
return the public key for the given context.`,
|
return the public key for the given context.`,
|
||||||
},
|
},
|
||||||
|
|
||||||
"auto_rotate_interval": {
|
"auto_rotate_period": {
|
||||||
Type: framework.TypeDurationSecond,
|
Type: framework.TypeDurationSecond,
|
||||||
Default: 0,
|
Default: 0,
|
||||||
Description: `Amount of time the key should live before
|
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)
|
keyType := d.Get("type").(string)
|
||||||
exportable := d.Get("exportable").(bool)
|
exportable := d.Get("exportable").(bool)
|
||||||
allowPlaintextBackup := d.Get("allow_plaintext_backup").(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 {
|
if autoRotatePeriod != 0 && autoRotatePeriod < time.Hour {
|
||||||
return logical.ErrorResponse("auto rotate interval must be 0 to disable or at least an hour"), nil
|
return logical.ErrorResponse("auto rotate period must be 0 to disable or at least an hour"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !derived && convergent {
|
if !derived && convergent {
|
||||||
|
@ -150,7 +150,7 @@ func (b *backend) pathPolicyWrite(ctx context.Context, req *logical.Request, d *
|
||||||
Convergent: convergent,
|
Convergent: convergent,
|
||||||
Exportable: exportable,
|
Exportable: exportable,
|
||||||
AllowPlaintextBackup: allowPlaintextBackup,
|
AllowPlaintextBackup: allowPlaintextBackup,
|
||||||
AutoRotateInterval: autoRotateInterval,
|
AutoRotatePeriod: autoRotatePeriod,
|
||||||
}
|
}
|
||||||
switch keyType {
|
switch keyType {
|
||||||
case "aes128-gcm96":
|
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_decryption": p.Type.DecryptionSupported(),
|
||||||
"supports_signing": p.Type.SigningSupported(),
|
"supports_signing": p.Type.SigningSupported(),
|
||||||
"supports_derivation": p.Type.DerivationSupported(),
|
"supports_derivation": p.Type.DerivationSupported(),
|
||||||
"auto_rotate_interval": int64(p.AutoRotateInterval.Seconds()),
|
"auto_rotate_period": int64(p.AutoRotatePeriod.Seconds()),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -95,7 +95,7 @@ func TestTransit_Issue_2958(t *testing.T) {
|
||||||
|
|
||||||
func TestTransit_CreateKeyWithAutorotation(t *testing.T) {
|
func TestTransit_CreateKeyWithAutorotation(t *testing.T) {
|
||||||
tests := map[string]struct {
|
tests := map[string]struct {
|
||||||
autoRotateInterval interface{}
|
autoRotatePeriod interface{}
|
||||||
shouldError bool
|
shouldError bool
|
||||||
expectedValue time.Duration
|
expectedValue time.Duration
|
||||||
}{
|
}{
|
||||||
|
@ -103,30 +103,30 @@ func TestTransit_CreateKeyWithAutorotation(t *testing.T) {
|
||||||
shouldError: false,
|
shouldError: false,
|
||||||
},
|
},
|
||||||
"0 (int)": {
|
"0 (int)": {
|
||||||
autoRotateInterval: 0,
|
autoRotatePeriod: 0,
|
||||||
shouldError: false,
|
shouldError: false,
|
||||||
expectedValue: 0,
|
expectedValue: 0,
|
||||||
},
|
},
|
||||||
"0 (string)": {
|
"0 (string)": {
|
||||||
autoRotateInterval: "0",
|
autoRotatePeriod: "0",
|
||||||
shouldError: false,
|
shouldError: false,
|
||||||
expectedValue: 0,
|
expectedValue: 0,
|
||||||
},
|
},
|
||||||
"5 seconds": {
|
"5 seconds": {
|
||||||
autoRotateInterval: "5s",
|
autoRotatePeriod: "5s",
|
||||||
shouldError: true,
|
shouldError: true,
|
||||||
},
|
},
|
||||||
"5 hours": {
|
"5 hours": {
|
||||||
autoRotateInterval: "5h",
|
autoRotatePeriod: "5h",
|
||||||
shouldError: false,
|
shouldError: false,
|
||||||
expectedValue: 5 * time.Hour,
|
expectedValue: 5 * time.Hour,
|
||||||
},
|
},
|
||||||
"negative value": {
|
"negative value": {
|
||||||
autoRotateInterval: "-1800s",
|
autoRotatePeriod: "-1800s",
|
||||||
shouldError: true,
|
shouldError: true,
|
||||||
},
|
},
|
||||||
"invalid string": {
|
"invalid string": {
|
||||||
autoRotateInterval: "this shouldn't work",
|
autoRotatePeriod: "this shouldn't work",
|
||||||
shouldError: true,
|
shouldError: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -160,7 +160,7 @@ func TestTransit_CreateKeyWithAutorotation(t *testing.T) {
|
||||||
keyName := hex.EncodeToString(keyNameBytes)
|
keyName := hex.EncodeToString(keyNameBytes)
|
||||||
|
|
||||||
_, err = client.Logical().Write(fmt.Sprintf("transit/keys/%s", keyName), map[string]interface{}{
|
_, err = client.Logical().Write(fmt.Sprintf("transit/keys/%s", keyName), map[string]interface{}{
|
||||||
"auto_rotate_interval": test.autoRotateInterval,
|
"auto_rotate_period": test.autoRotatePeriod,
|
||||||
})
|
})
|
||||||
switch {
|
switch {
|
||||||
case test.shouldError && err == nil:
|
case test.shouldError && err == nil:
|
||||||
|
@ -177,7 +177,7 @@ func TestTransit_CreateKeyWithAutorotation(t *testing.T) {
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
t.Fatal("expected non-nil response")
|
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 {
|
if !ok {
|
||||||
t.Fatal("returned value is of unexpected type")
|
t.Fatal("returned value is of unexpected type")
|
||||||
}
|
}
|
||||||
|
@ -187,7 +187,7 @@ func TestTransit_CreateKeyWithAutorotation(t *testing.T) {
|
||||||
}
|
}
|
||||||
want := int64(test.expectedValue.Seconds())
|
want := int64(test.expectedValue.Seconds())
|
||||||
if got != want {
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
```release-note:improvement
|
|
||||||
ui: Adds multi-factor authentication support
|
|
||||||
```
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
api: Define constants for X-Vault-Forward and X-Vault-Inconsistent headers
|
||||||
|
```
|
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:bug
|
||||||
|
auth/approle: Fix wrapping of nil errors in `login` endpoint
|
||||||
|
```
|
|
@ -29,8 +29,8 @@ Usage: vault secrets move [options] SOURCE DESTINATION
|
||||||
secrets engine are revoked, but all configuration associated with the engine
|
secrets engine are revoked, but all configuration associated with the engine
|
||||||
is preserved.
|
is preserved.
|
||||||
|
|
||||||
This command only works within a namespace; it cannot be used to move engines
|
This command works within or across namespaces, both source and destination paths
|
||||||
to different namespaces.
|
can be prefixed with a namespace heirarchy relative to the current namespace.
|
||||||
|
|
||||||
WARNING! Moving an existing secrets engine will revoke any leases from the
|
WARNING! Moving an existing secrets engine will revoke any leases from the
|
||||||
old engine.
|
old engine.
|
||||||
|
@ -39,6 +39,11 @@ Usage: vault secrets move [options] SOURCE DESTINATION
|
||||||
|
|
||||||
$ vault secrets move secret/ generic/
|
$ 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()
|
` + c.Flags().Help()
|
||||||
|
|
||||||
return strings.TrimSpace(helpText)
|
return strings.TrimSpace(helpText)
|
||||||
|
@ -84,11 +89,12 @@ func (c *SecretsMoveCommand) Run(args []string) int {
|
||||||
return 2
|
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))
|
c.UI.Error(fmt.Sprintf("Error moving secrets engine %s to %s: %s", source, destination, err))
|
||||||
return 2
|
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
|
return 0
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ package command
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
)
|
)
|
||||||
|
@ -91,12 +92,16 @@ func TestSecretsMoveCommand_Run(t *testing.T) {
|
||||||
t.Errorf("expected %d to be %d", code, exp)
|
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()
|
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
|
||||||
if !strings.Contains(combined, expected) {
|
if !strings.Contains(combined, expected) {
|
||||||
t.Errorf("expected %q to contain %q", 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()
|
mounts, err := client.Sys().ListMounts()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -306,6 +306,7 @@ require (
|
||||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||||
github.com/miekg/dns v1.1.41 // 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/hashstructure v1.0.0 // indirect
|
||||||
github.com/mitchellh/iochan v1.0.0 // indirect
|
github.com/mitchellh/iochan v1.0.0 // indirect
|
||||||
github.com/mitchellh/pointerstructure v1.2.0 // indirect
|
github.com/mitchellh/pointerstructure v1.2.0 // indirect
|
||||||
|
|
2
go.sum
2
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 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY=
|
||||||
github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
|
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/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/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.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||||
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
|
github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
|
||||||
|
|
|
@ -133,3 +133,20 @@ func SplitIDFromString(input string) (string, string) {
|
||||||
|
|
||||||
return prefix + input[:idx], input[idx+1:]
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -2,8 +2,10 @@ package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/go-test/deep"
|
"github.com/go-test/deep"
|
||||||
|
|
||||||
|
@ -374,8 +376,24 @@ func TestSysRemount(t *testing.T) {
|
||||||
"from": "foo",
|
"from": "foo",
|
||||||
"to": "bar",
|
"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")
|
resp = testHttpGet(t, token, addr+"/v1/sys/mounts")
|
||||||
|
|
||||||
var actual map[string]interface{}
|
var actual map[string]interface{}
|
||||||
|
|
|
@ -52,7 +52,7 @@ type PolicyRequest struct {
|
||||||
AllowPlaintextBackup bool
|
AllowPlaintextBackup bool
|
||||||
|
|
||||||
// How frequently the key should automatically rotate
|
// How frequently the key should automatically rotate
|
||||||
AutoRotateInterval time.Duration
|
AutoRotatePeriod time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
type LockManager struct {
|
type LockManager struct {
|
||||||
|
@ -383,7 +383,7 @@ func (lm *LockManager) GetPolicy(ctx context.Context, req PolicyRequest, rand io
|
||||||
Derived: req.Derived,
|
Derived: req.Derived,
|
||||||
Exportable: req.Exportable,
|
Exportable: req.Exportable,
|
||||||
AllowPlaintextBackup: req.AllowPlaintextBackup,
|
AllowPlaintextBackup: req.AllowPlaintextBackup,
|
||||||
AutoRotateInterval: req.AutoRotateInterval,
|
AutoRotatePeriod: req.AutoRotatePeriod,
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.Derived {
|
if req.Derived {
|
||||||
|
|
|
@ -374,9 +374,9 @@ type Policy struct {
|
||||||
// policy object.
|
// policy object.
|
||||||
StoragePrefix string `json:"storage_prefix"`
|
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.
|
// 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
|
// versionPrefixCache stores caches of version prefix strings and the split
|
||||||
// version template.
|
// version template.
|
||||||
|
|
|
@ -126,19 +126,6 @@ export default ApplicationAdapter.extend({
|
||||||
return this.ajax(url, verb, options);
|
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) {
|
urlFor(endpoint) {
|
||||||
if (!ENDPOINTS.includes(endpoint)) {
|
if (!ENDPOINTS.includes(endpoint)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|
|
@ -18,13 +18,13 @@ const BACKENDS = supportedAuthBackends();
|
||||||
*
|
*
|
||||||
* @example ```js
|
* @example ```js
|
||||||
* // All properties are passed in via query params.
|
* // All properties are passed in via query params.
|
||||||
* <AuthForm @wrappedToken={{wrappedToken}} @cluster={{model}} @namespace={{namespaceQueryParam}} @selectedAuth={{authMethod}} @onSuccess={{action this.onSuccess}} />```
|
* <AuthForm @wrappedToken={{wrappedToken}} @cluster={{model}} @namespace={{namespaceQueryParam}} @redirectTo={{redirectTo}} @selectedAuth={{authMethod}}/>```
|
||||||
*
|
*
|
||||||
* @param {string} wrappedToken - The auth method that is currently selected in the dropdown.
|
* @param wrappedToken=null {String} - 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 cluster=null {Object} - The auth method that is currently selected in the dropdown. This corresponds to an Ember Model.
|
||||||
* @param {string} namespace- The currently active namespace.
|
* @param namespace=null {String} - The currently active namespace.
|
||||||
* @param {string} selectedAuth - The auth method that is currently selected in the dropdown.
|
* @param redirectTo=null {String} - The name of the route to redirect to.
|
||||||
* @param {function} onSuccess - Fired on auth success
|
* @param selectedAuth=null {String} - The auth method that is currently selected in the dropdown.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const DEFAULTS = {
|
const DEFAULTS = {
|
||||||
|
@ -45,6 +45,7 @@ export default Component.extend(DEFAULTS, {
|
||||||
selectedAuth: null,
|
selectedAuth: null,
|
||||||
methods: null,
|
methods: null,
|
||||||
cluster: null,
|
cluster: null,
|
||||||
|
redirectTo: null,
|
||||||
namespace: null,
|
namespace: null,
|
||||||
wrappedToken: null,
|
wrappedToken: null,
|
||||||
// internal
|
// internal
|
||||||
|
@ -205,18 +206,54 @@ export default Component.extend(DEFAULTS, {
|
||||||
|
|
||||||
showLoading: or('isLoading', 'authenticate.isRunning', 'fetchMethods.isRunning', 'unwrapToken.isRunning'),
|
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(
|
authenticate: task(
|
||||||
waitFor(function* (backendType, data) {
|
waitFor(function* (backendType, data) {
|
||||||
let clusterId = this.cluster.id;
|
let clusterId = this.cluster.id;
|
||||||
try {
|
try {
|
||||||
|
if (backendType === 'okta') {
|
||||||
this.delayAuthMessageReminder.perform();
|
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)}`);
|
|
||||||
}
|
}
|
||||||
|
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) {
|
if (Ember.testing) {
|
||||||
this.showLoading = true;
|
this.showLoading = true;
|
||||||
yield timeout(0);
|
yield timeout(0);
|
||||||
} else {
|
return;
|
||||||
yield timeout(5000);
|
|
||||||
}
|
}
|
||||||
|
yield timeout(5000);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
|
@ -261,10 +298,11 @@ export default Component.extend(DEFAULTS, {
|
||||||
return this.authenticate.unlinked().perform(backend.type, data);
|
return this.authenticate.unlinked().perform(backend.type, data);
|
||||||
},
|
},
|
||||||
handleError(e) {
|
handleError(e) {
|
||||||
this.setProperties({
|
if (e) {
|
||||||
loading: false,
|
this.handleError(e, false);
|
||||||
error: e ? this.auth.handleError(e) : null,
|
} else {
|
||||||
});
|
this.set('error', null);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,6 +15,9 @@ export default class Current extends Component {
|
||||||
return { name: namespace['label'], id: namespace['label'] };
|
return { name: namespace['label'], id: namespace['label'] };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@tracked selectedAuthMethod = null;
|
||||||
|
@tracked authMethodOptions = [];
|
||||||
|
|
||||||
// Response client count data by namespace for current/partial month
|
// Response client count data by namespace for current/partial month
|
||||||
get byNamespaceCurrent() {
|
get byNamespaceCurrent() {
|
||||||
return this.args.model.monthly?.byNamespace || [];
|
return this.args.model.monthly?.byNamespace || [];
|
||||||
|
@ -26,7 +29,21 @@ export default class Current extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasAttributionData() {
|
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() {
|
get countsIncludeOlderData() {
|
||||||
|
@ -41,16 +58,13 @@ export default class Current extends Component {
|
||||||
|
|
||||||
// top level TOTAL client counts for current/partial month
|
// top level TOTAL client counts for current/partial month
|
||||||
get totalUsageCounts() {
|
get totalUsageCounts() {
|
||||||
return this.selectedNamespace
|
return this.selectedNamespace ? this.filteredActivity : this.args.model.monthly?.total;
|
||||||
? this.filterByNamespace(this.selectedNamespace)
|
|
||||||
: this.args.model.monthly?.total;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// total client data for horizontal bar chart in attribution component
|
// total client data for horizontal bar chart in attribution component
|
||||||
get totalClientsData() {
|
get totalClientsData() {
|
||||||
if (this.selectedNamespace) {
|
if (this.selectedNamespace) {
|
||||||
let filteredNamespace = this.filterByNamespace(this.selectedNamespace);
|
return this.filteredActivity?.mounts || null;
|
||||||
return filteredNamespace.mounts ? this.filterByNamespace(this.selectedNamespace).mounts : null;
|
|
||||||
} else {
|
} else {
|
||||||
return this.byNamespaceCurrent;
|
return this.byNamespaceCurrent;
|
||||||
}
|
}
|
||||||
|
@ -60,15 +74,26 @@ export default class Current extends Component {
|
||||||
return this.args.model.monthly?.responseTimestamp;
|
return this.args.model.monthly?.responseTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
// HELPERS
|
|
||||||
filterByNamespace(namespace) {
|
|
||||||
return this.byNamespaceCurrent.find((ns) => ns.label === namespace);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ACTIONS
|
// ACTIONS
|
||||||
@action
|
@action
|
||||||
selectNamespace([value]) {
|
selectNamespace([value]) {
|
||||||
// value comes in as [namespace0]
|
// value comes in as [namespace0]
|
||||||
this.selectedNamespace = value;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,10 +38,15 @@ export default class History extends Component {
|
||||||
years = Array.from({ length: 5 }, (item, i) => {
|
years = Array.from({ length: 5 }, (item, i) => {
|
||||||
return new Date().getFullYear() - 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 isEditStartMonthOpen = false;
|
||||||
@tracked startMonth = null;
|
@tracked startMonth = null;
|
||||||
@tracked startYear = null;
|
@tracked startYear = null;
|
||||||
|
@tracked allowedMonthMax = 12;
|
||||||
|
@tracked disabledYear = null;
|
||||||
|
|
||||||
// FOR HISTORY COMPONENT //
|
// FOR HISTORY COMPONENT //
|
||||||
|
|
||||||
|
@ -57,14 +62,19 @@ export default class History extends Component {
|
||||||
|
|
||||||
// SEARCH SELECT
|
// SEARCH SELECT
|
||||||
@tracked selectedNamespace = null;
|
@tracked selectedNamespace = null;
|
||||||
@tracked namespaceArray = this.getActivityResponse.byNamespace.map((namespace) => {
|
@tracked namespaceArray = this.getActivityResponse.byNamespace.map((namespace) => ({
|
||||||
return { name: namespace['label'], id: namespace['label'] };
|
name: namespace.label,
|
||||||
});
|
id: namespace.label,
|
||||||
|
}));
|
||||||
|
|
||||||
// TEMPLATE MESSAGING
|
// TEMPLATE MESSAGING
|
||||||
@tracked noActivityDate = '';
|
@tracked noActivityDate = '';
|
||||||
@tracked responseRangeDiffMessage = null;
|
@tracked responseRangeDiffMessage = null;
|
||||||
@tracked isLoadingQuery = false;
|
@tracked isLoadingQuery = false;
|
||||||
|
@tracked licenseStartIsCurrentMonth = this.args.model.activity?.isLicenseDateError || false;
|
||||||
|
|
||||||
|
@tracked selectedAuthMethod = null;
|
||||||
|
@tracked authMethodOptions = [];
|
||||||
|
|
||||||
get versionText() {
|
get versionText() {
|
||||||
return this.version.isEnterprise
|
return this.version.isEnterprise
|
||||||
|
@ -92,7 +102,7 @@ export default class History extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasAttributionData() {
|
get hasAttributionData() {
|
||||||
return this.totalUsageCounts.clients !== 0 && this.totalClientsData.length !== 0;
|
return this.totalUsageCounts.clients !== 0 && !!this.totalClientsData && !this.selectedAuthMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
get startTimeDisplay() {
|
get startTimeDisplay() {
|
||||||
|
@ -113,6 +123,20 @@ export default class History extends Component {
|
||||||
return `${this.arrayOfMonths[month]} ${year}`;
|
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() {
|
get isDateRange() {
|
||||||
return !isSameMonth(
|
return !isSameMonth(
|
||||||
new Date(this.getActivityResponse.startTime),
|
new Date(this.getActivityResponse.startTime),
|
||||||
|
@ -122,16 +146,13 @@ export default class History extends Component {
|
||||||
|
|
||||||
// top level TOTAL client counts for given date range
|
// top level TOTAL client counts for given date range
|
||||||
get totalUsageCounts() {
|
get totalUsageCounts() {
|
||||||
return this.selectedNamespace
|
return this.selectedNamespace ? this.filteredActivity : this.getActivityResponse.total;
|
||||||
? this.filterByNamespace(this.selectedNamespace)
|
|
||||||
: this.getActivityResponse.total;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// total client data for horizontal bar chart in attribution component
|
// total client data for horizontal bar chart in attribution component
|
||||||
get totalClientsData() {
|
get totalClientsData() {
|
||||||
if (this.selectedNamespace) {
|
if (this.selectedNamespace) {
|
||||||
let filteredNamespace = this.filterByNamespace(this.selectedNamespace);
|
return this.filteredActivity?.mounts || null;
|
||||||
return filteredNamespace.mounts ? this.filterByNamespace(this.selectedNamespace).mounts : null;
|
|
||||||
} else {
|
} else {
|
||||||
return this.getActivityResponse?.byNamespace;
|
return this.getActivityResponse?.byNamespace;
|
||||||
}
|
}
|
||||||
|
@ -157,6 +178,7 @@ export default class History extends Component {
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async handleClientActivityQuery(month, year, dateType) {
|
async handleClientActivityQuery(month, year, dateType) {
|
||||||
|
this.isEditStartMonthOpen = false;
|
||||||
if (dateType === 'cancel') {
|
if (dateType === 'cancel') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -195,6 +217,7 @@ export default class History extends Component {
|
||||||
this.storage().setItem(INPUTTED_START_DATE, this.startTimeFromResponse);
|
this.storage().setItem(INPUTTED_START_DATE, this.startTimeFromResponse);
|
||||||
}
|
}
|
||||||
this.queriedActivityResponse = response;
|
this.queriedActivityResponse = response;
|
||||||
|
this.licenseStartIsCurrentMonth = response.isLicenseDateError;
|
||||||
// compare if the response startTime comes after the requested startTime. If true throw a warning.
|
// compare if the response startTime comes after the requested startTime. If true throw a warning.
|
||||||
// only display if they selected a startTime
|
// only display if they selected a startTime
|
||||||
if (
|
if (
|
||||||
|
@ -209,7 +232,6 @@ export default class History extends Component {
|
||||||
this.responseRangeDiffMessage = null;
|
this.responseRangeDiffMessage = null;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// TODO CMB surface API errors when user selects start date after end date
|
|
||||||
return e;
|
return e;
|
||||||
} finally {
|
} finally {
|
||||||
this.isLoadingQuery = false;
|
this.isLoadingQuery = false;
|
||||||
|
@ -225,22 +247,38 @@ export default class History extends Component {
|
||||||
selectNamespace([value]) {
|
selectNamespace([value]) {
|
||||||
// value comes in as [namespace0]
|
// value comes in as [namespace0]
|
||||||
this.selectedNamespace = value;
|
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
|
// FOR START DATE MODAL
|
||||||
@action
|
@action
|
||||||
selectStartMonth(month) {
|
selectStartMonth(month, event) {
|
||||||
this.startMonth = month;
|
this.startMonth = month;
|
||||||
|
// disables months if in the future
|
||||||
|
this.disabledYear = this.months.indexOf(month) >= this.currentMonth ? this.currentYear : null;
|
||||||
|
event.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
selectStartYear(year) {
|
selectStartYear(year, event) {
|
||||||
this.startYear = 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() {
|
storage() {
|
||||||
|
|
|
@ -12,9 +12,15 @@ import { tracked } from '@glimmer/tracking';
|
||||||
* ```
|
* ```
|
||||||
* @param {function} handleDateSelection - is the action from the parent that the date picker triggers
|
* @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} [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 {
|
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 startMonth = null;
|
||||||
@tracked startYear = null;
|
@tracked startYear = null;
|
||||||
|
|
||||||
|
@ -26,13 +32,18 @@ export default class DateDropdown extends Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
@action
|
@action
|
||||||
selectStartMonth(month) {
|
selectStartMonth(month, event) {
|
||||||
this.startMonth = month;
|
this.startMonth = month;
|
||||||
|
// disables months if in the future
|
||||||
|
this.disabledYear = this.months.indexOf(month) >= this.currentMonth ? this.currentYear : null;
|
||||||
|
event.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
selectStartYear(year) {
|
selectStartYear(year, event) {
|
||||||
this.startYear = year;
|
this.startYear = year;
|
||||||
|
this.allowedMonthMax = year === this.currentYear ? this.currentMonth : 12;
|
||||||
|
event.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
|
|
|
@ -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
|
|
||||||
* <MfaError />
|
|
||||||
* ```
|
|
||||||
*/
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
* <MfaForm @clusterId={this.model.id} @authData={this.authData} />
|
|
||||||
* ```
|
|
||||||
* @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();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -95,10 +95,10 @@ export default Component.extend(FocusOnInsertMixin, {
|
||||||
|
|
||||||
handleAutoRotateChange(ttlObj) {
|
handleAutoRotateChange(ttlObj) {
|
||||||
if (ttlObj.enabled) {
|
if (ttlObj.enabled) {
|
||||||
set(this.key, 'autoRotateInterval', ttlObj.goSafeTimeString);
|
set(this.key, 'autoRotatePeriod', ttlObj.goSafeTimeString);
|
||||||
this.set('autoRotateInvalid', ttlObj.seconds < 3600);
|
this.set('autoRotateInvalid', ttlObj.seconds < 3600);
|
||||||
} else {
|
} else {
|
||||||
set(this.key, 'autoRotateInterval', 0);
|
set(this.key, 'autoRotatePeriod', 0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -8,18 +8,13 @@ export default Controller.extend({
|
||||||
clusterController: controller('vault.cluster'),
|
clusterController: controller('vault.cluster'),
|
||||||
namespaceService: service('namespace'),
|
namespaceService: service('namespace'),
|
||||||
featureFlagService: service('featureFlag'),
|
featureFlagService: service('featureFlag'),
|
||||||
auth: service(),
|
|
||||||
router: service(),
|
|
||||||
|
|
||||||
queryParams: [{ authMethod: 'with', oidcProvider: 'o' }],
|
|
||||||
|
|
||||||
namespaceQueryParam: alias('clusterController.namespaceQueryParam'),
|
namespaceQueryParam: alias('clusterController.namespaceQueryParam'),
|
||||||
|
queryParams: [{ authMethod: 'with', oidcProvider: 'o' }],
|
||||||
wrappedToken: alias('vaultController.wrappedToken'),
|
wrappedToken: alias('vaultController.wrappedToken'),
|
||||||
redirectTo: alias('vaultController.redirectTo'),
|
|
||||||
managedNamespaceRoot: alias('featureFlagService.managedNamespaceRoot'),
|
|
||||||
|
|
||||||
authMethod: '',
|
authMethod: '',
|
||||||
oidcProvider: '',
|
oidcProvider: '',
|
||||||
|
redirectTo: alias('vaultController.redirectTo'),
|
||||||
|
managedNamespaceRoot: alias('featureFlagService.managedNamespaceRoot'),
|
||||||
|
|
||||||
get managedNamespaceChild() {
|
get managedNamespaceChild() {
|
||||||
let fullParam = this.namespaceQueryParam;
|
let fullParam = this.namespaceQueryParam;
|
||||||
|
@ -46,39 +41,4 @@ export default Controller.extend({
|
||||||
this.namespaceService.setNamespace(value, true);
|
this.namespaceService.setNamespace(value, true);
|
||||||
this.set('namespaceQueryParam', value);
|
this.set('namespaceQueryParam', value);
|
||||||
}).restartable(),
|
}).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);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
import { helper } from '@ember/component/helper';
|
import { helper } from '@ember/component/helper';
|
||||||
import { formatDuration, intervalToDuration } from 'date-fns';
|
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:
|
// intervalToDuration creates a durationObject that turns the seconds (ex 3600) to respective:
|
||||||
// { years: 0, months: 0, days: 0, hours: 1, minutes: 0, seconds: 0 }
|
// { years: 0, months: 0, days: 0, hours: 1, minutes: 0, seconds: 0 }
|
||||||
// then formatDuration returns the filled in keys of the durationObject
|
// then formatDuration returns the filled in keys of the durationObject
|
||||||
|
|
||||||
|
if (removeZero && time === '0') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// time must be in seconds
|
// time must be in seconds
|
||||||
let duration = Number.parseInt(time, 10);
|
let duration = Number.parseInt(time, 10);
|
||||||
if (isNaN(duration)) {
|
if (isNaN(duration)) {
|
||||||
|
|
|
@ -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);
|
|
||||||
});
|
|
|
@ -56,11 +56,11 @@ export default Model.extend({
|
||||||
fieldValue: 'id',
|
fieldValue: 'id',
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
}),
|
}),
|
||||||
autoRotateInterval: attr({
|
autoRotatePeriod: attr({
|
||||||
defaultValue: '0',
|
defaultValue: '0',
|
||||||
defaultShown: 'Key is not automatically rotated',
|
defaultShown: 'Key is not automatically rotated',
|
||||||
editType: 'ttl',
|
editType: 'ttl',
|
||||||
label: 'Auto-rotation interval',
|
label: 'Auto-rotation period',
|
||||||
}),
|
}),
|
||||||
deletionAllowed: attr('boolean'),
|
deletionAllowed: attr('boolean'),
|
||||||
derived: attr('boolean'),
|
derived: attr('boolean'),
|
||||||
|
|
|
@ -8,10 +8,13 @@ export default class HistoryRoute extends Route {
|
||||||
try {
|
try {
|
||||||
// on init ONLY make network request if we have a start time from the license
|
// on init ONLY make network request if we have a start time from the license
|
||||||
// otherwise user needs to manually input
|
// otherwise user needs to manually input
|
||||||
// TODO CMB what to return here?
|
|
||||||
return start_time ? await this.store.queryRecord('clients/activity', { start_time }) : {};
|
return start_time ? await this.store.queryRecord('clients/activity', { start_time }) : {};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return e;
|
// returns 400 when license start date is in the current month
|
||||||
|
if (e.httpStatus === 400) {
|
||||||
|
return { isLicenseDateError: true };
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ export default ApplicationSerializer.extend({
|
||||||
id: payload.id,
|
id: payload.id,
|
||||||
data: {
|
data: {
|
||||||
...payload.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);
|
return this._super(store, primaryModelClass, normalizedPayload, id, requestType);
|
||||||
|
|
|
@ -50,12 +50,12 @@ export default RESTSerializer.extend({
|
||||||
const min_decryption_version = snapshot.attr('minDecryptionVersion');
|
const min_decryption_version = snapshot.attr('minDecryptionVersion');
|
||||||
const min_encryption_version = snapshot.attr('minEncryptionVersion');
|
const min_encryption_version = snapshot.attr('minEncryptionVersion');
|
||||||
const deletion_allowed = snapshot.attr('deletionAllowed');
|
const deletion_allowed = snapshot.attr('deletionAllowed');
|
||||||
const auto_rotate_interval = snapshot.attr('autoRotateInterval');
|
const auto_rotate_period = snapshot.attr('autoRotatePeriod');
|
||||||
return {
|
return {
|
||||||
min_decryption_version,
|
min_decryption_version,
|
||||||
min_encryption_version,
|
min_encryption_version,
|
||||||
deletion_allowed,
|
deletion_allowed,
|
||||||
auto_rotate_interval,
|
auto_rotate_period,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
snapshot.id = snapshot.attr('name');
|
snapshot.id = snapshot.attr('name');
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { resolve, reject } from 'rsvp';
|
||||||
import { assign } from '@ember/polyfills';
|
import { assign } from '@ember/polyfills';
|
||||||
import { isArray } from '@ember/array';
|
import { isArray } from '@ember/array';
|
||||||
import { computed, get } from '@ember/object';
|
import { computed, get } from '@ember/object';
|
||||||
import { capitalize } from '@ember/string';
|
|
||||||
|
|
||||||
import fetch from 'fetch';
|
import fetch from 'fetch';
|
||||||
import { getOwner } from '@ember/application';
|
import { getOwner } from '@ember/application';
|
||||||
|
@ -15,10 +14,9 @@ import { task, timeout } from 'ember-concurrency';
|
||||||
const TOKEN_SEPARATOR = '☃';
|
const TOKEN_SEPARATOR = '☃';
|
||||||
const TOKEN_PREFIX = 'vault-';
|
const TOKEN_PREFIX = 'vault-';
|
||||||
const ROOT_PREFIX = '_root_';
|
const ROOT_PREFIX = '_root_';
|
||||||
const TOTP_NOT_CONFIGURED = 'TOTP mfa required but not configured';
|
|
||||||
const BACKENDS = supportedAuthBackends();
|
const BACKENDS = supportedAuthBackends();
|
||||||
|
|
||||||
export { TOKEN_SEPARATOR, TOKEN_PREFIX, ROOT_PREFIX, TOTP_NOT_CONFIGURED };
|
export { TOKEN_SEPARATOR, TOKEN_PREFIX, ROOT_PREFIX };
|
||||||
|
|
||||||
export default Service.extend({
|
export default Service.extend({
|
||||||
permissions: service(),
|
permissions: service(),
|
||||||
|
@ -26,8 +24,6 @@ export default Service.extend({
|
||||||
IDLE_TIMEOUT: 3 * 60e3,
|
IDLE_TIMEOUT: 3 * 60e3,
|
||||||
expirationCalcTS: null,
|
expirationCalcTS: null,
|
||||||
isRenewing: false,
|
isRenewing: false,
|
||||||
mfaErrors: null,
|
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.checkForRootToken();
|
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}*/) {
|
async authenticate(/*{clusterId, backend, data}*/) {
|
||||||
const [options] = arguments;
|
const [options] = arguments;
|
||||||
const adapter = this.clusterAdapter();
|
const adapter = this.clusterAdapter();
|
||||||
let resp;
|
|
||||||
|
|
||||||
try {
|
let resp = await adapter.authenticate(options);
|
||||||
resp = await adapter.authenticate(options);
|
let authData = await this.persistAuthData(options, resp.auth || resp.data, this.namespaceService.path);
|
||||||
} 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);
|
|
||||||
await this.permissions.getPaths.perform();
|
await this.permissions.getPaths.perform();
|
||||||
return authData;
|
return authData;
|
||||||
},
|
},
|
||||||
|
|
||||||
handleError(e) {
|
|
||||||
if (e.errors) {
|
|
||||||
return e.errors.map((error) => {
|
|
||||||
if (error.detail) {
|
|
||||||
return error.detail;
|
|
||||||
}
|
|
||||||
return error;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return [e];
|
|
||||||
},
|
|
||||||
|
|
||||||
getAuthType() {
|
getAuthType() {
|
||||||
if (!this.authData) return;
|
if (!this.authData) return;
|
||||||
return this.authData.backend.type;
|
return this.authData.backend.type;
|
||||||
|
|
|
@ -51,7 +51,3 @@
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-blue {
|
|
||||||
color: $blue;
|
|
||||||
}
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ $button-box-shadow-standard: 0 3px 1px 0 rgba($black, 0.12);
|
||||||
background-color: $color;
|
background-color: $color;
|
||||||
color: $color-invert;
|
color: $color-invert;
|
||||||
|
|
||||||
&:hover,
|
&:hover:not([disabled]),
|
||||||
&.is-hovered {
|
&.is-hovered {
|
||||||
background-color: darken($color, 5%);
|
background-color: darken($color, 5%);
|
||||||
border-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;
|
padding: $size-8;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-button {
|
|
||||||
background: transparent;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
border: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
|
@ -19,9 +19,6 @@
|
||||||
.is-borderless {
|
.is-borderless {
|
||||||
border: none !important;
|
border: none !important;
|
||||||
}
|
}
|
||||||
.is-box-shadowless {
|
|
||||||
box-shadow: none !important;
|
|
||||||
}
|
|
||||||
.is-relative {
|
.is-relative {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
@ -191,9 +188,6 @@
|
||||||
.has-top-margin-xl {
|
.has-top-margin-xl {
|
||||||
margin-top: $spacing-xl;
|
margin-top: $spacing-xl;
|
||||||
}
|
}
|
||||||
.has-top-margin-xxl {
|
|
||||||
margin-top: $spacing-xxl;
|
|
||||||
}
|
|
||||||
.has-border-bottom-light {
|
.has-border-bottom-light {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
border-bottom: 1px solid $grey-light;
|
border-bottom: 1px solid $grey-light;
|
||||||
|
@ -210,9 +204,7 @@ ul.bullet {
|
||||||
.has-text-semibold {
|
.has-text-semibold {
|
||||||
font-weight: $font-weight-semibold;
|
font-weight: $font-weight-semibold;
|
||||||
}
|
}
|
||||||
.is-v-centered {
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
.has-text-grey-400 {
|
.has-text-grey-400 {
|
||||||
color: $ui-gray-400;
|
color: $ui-gray-400;
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,20 @@
|
||||||
@onChange={{this.selectNamespace}}
|
@onChange={{this.selectNamespace}}
|
||||||
@placeholder={{"Filter by namespace"}}
|
@placeholder={{"Filter by namespace"}}
|
||||||
@displayInherit={{true}}
|
@displayInherit={{true}}
|
||||||
|
class="is-marginless"
|
||||||
/>
|
/>
|
||||||
|
{{#if this.selectedNamespace}}
|
||||||
|
<SearchSelect
|
||||||
|
@id="auth-method-search-select"
|
||||||
|
@options={{this.authMethodOptions}}
|
||||||
|
@selectLimit="1"
|
||||||
|
@disallowNewItems={{true}}
|
||||||
|
@fallbackComponent="input-search"
|
||||||
|
@onChange={{this.setAuthMethod}}
|
||||||
|
@placeholder={{"Filter by auth method"}}
|
||||||
|
@displayInherit={{true}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
</ToolbarFilters>
|
</ToolbarFilters>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,13 +14,22 @@
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
{{else}}
|
{{else}}
|
||||||
<DateDropdown @handleDateSelection={{this.handleClientActivityQuery}} @name={{"startTime"}} />
|
<DateDropdown @handleDateSelection={{this.handleClientActivityQuery}} @name={{"startTime"}} @submitText="Save" />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</div>
|
</div>
|
||||||
<p class="is-8 has-text-grey has-bottom-margin-xl">
|
<p class="is-8 has-text-grey has-bottom-margin-xl">
|
||||||
{{this.versionText.description}}
|
{{this.versionText.description}}
|
||||||
</p>
|
</p>
|
||||||
{{#if (eq @model.config.queriesAvailable false)}}
|
{{#if this.licenseStartIsCurrentMonth}}
|
||||||
|
<EmptyState
|
||||||
|
@title="No data for this billing period"
|
||||||
|
@subTitle="Your billing period has just begun, so there is no data yet. Data will be available here on the first of next month."
|
||||||
|
@message="To view data from a previous billing period, you can enter your previous billing start date."
|
||||||
|
@bottomBorder={{true}}
|
||||||
|
>
|
||||||
|
<DateDropdown @handleDateSelection={{this.handleClientActivityQuery}} @name={{"startTime"}} @submitText="View" />
|
||||||
|
</EmptyState>
|
||||||
|
{{else if (eq @model.config.queriesAvailable false)}}
|
||||||
{{#if (eq @model.config.enabled "On")}}
|
{{#if (eq @model.config.enabled "On")}}
|
||||||
<EmptyState
|
<EmptyState
|
||||||
@title={{concat "No monthly history " (if this.noActivityDate "from ") this.noActivityDate}}
|
@title={{concat "No monthly history " (if this.noActivityDate "from ") this.noActivityDate}}
|
||||||
|
@ -74,6 +83,19 @@
|
||||||
@onChange={{this.selectNamespace}}
|
@onChange={{this.selectNamespace}}
|
||||||
@placeholder={{"Filter by namespace"}}
|
@placeholder={{"Filter by namespace"}}
|
||||||
@displayInherit={{true}}
|
@displayInherit={{true}}
|
||||||
|
class="is-marginless"
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
{{#if this.selectedNamespace}}
|
||||||
|
<SearchSelect
|
||||||
|
@id="auth-method-search-select"
|
||||||
|
@options={{this.authMethodOptions}}
|
||||||
|
@selectLimit="1"
|
||||||
|
@disallowNewItems={{true}}
|
||||||
|
@fallbackComponent="input-search"
|
||||||
|
@onChange={{this.setAuthMethod}}
|
||||||
|
@placeholder={{"Filter by auth method"}}
|
||||||
|
@displayInherit={{true}}
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</ToolbarFilters>
|
</ToolbarFilters>
|
||||||
|
@ -125,8 +147,10 @@
|
||||||
<EmptyState @title="No data received" @message="No data exists for that query period. Try searching again." />
|
<EmptyState @title="No data received" @message="No data exists for that query period. Try searching again." />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{else}}
|
{{else if (or (not @model.startTimeFromLicense) (not this.startTimeFromResponse))}}
|
||||||
<EmptyState @title={{this.versionText.title}} @message={{this.versionText.message}} />
|
<EmptyState @title={{this.versionText.title}} @message={{this.versionText.message}} />
|
||||||
|
{{else}}
|
||||||
|
<EmptyState @title="No data received" @message="No data exists for that query period. Try searching again." />
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
@ -155,11 +179,12 @@
|
||||||
<D.Content @defaultClass="popup-menu-content is-wide">
|
<D.Content @defaultClass="popup-menu-content is-wide">
|
||||||
<nav class="box menu scroll">
|
<nav class="box menu scroll">
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
{{#each this.months as |month|}}
|
{{#each this.months as |month index|}}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="link"
|
class="button link"
|
||||||
{{on "click" (queue (fn this.selectStartMonth month) (action D.actions.close))}}
|
disabled={{if (lt index this.allowedMonthMax) false true}}
|
||||||
|
{{on "click" (fn this.selectStartMonth month D.actions)}}
|
||||||
>
|
>
|
||||||
{{month}}
|
{{month}}
|
||||||
</button>
|
</button>
|
||||||
|
@ -183,8 +208,9 @@
|
||||||
{{#each this.years as |year|}}
|
{{#each this.years as |year|}}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="link"
|
class="button link"
|
||||||
{{on "click" (queue (fn this.selectStartYear year) (action D.actions.close))}}
|
disabled={{if (eq year this.disabledYear) true false}}
|
||||||
|
{{on "click" (fn this.selectStartYear year D.actions)}}
|
||||||
>
|
>
|
||||||
{{year}}
|
{{year}}
|
||||||
</button>
|
</button>
|
||||||
|
@ -199,22 +225,12 @@
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="button is-primary"
|
class="button is-primary"
|
||||||
onclick={{queue
|
disabled={{or (if (and this.startMonth this.startYear) false true)}}
|
||||||
(action (mut this.isEditStartMonthOpen) false)
|
{{on "click" (fn this.handleClientActivityQuery this.startMonth this.startYear "startTime")}}
|
||||||
(action "handleClientActivityQuery" this.startMonth this.startYear "startTime")
|
|
||||||
}}
|
|
||||||
disabled={{if (and this.startMonth this.startYear) false true}}
|
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="button" class="button is-secondary" {{on "click" (fn this.handleClientActivityQuery 0 0 "cancel")}}>
|
||||||
type="button"
|
|
||||||
class="button is-secondary"
|
|
||||||
{{on
|
|
||||||
"click"
|
|
||||||
(queue (action (mut this.isEditStartMonthOpen) false) (fn this.handleClientActivityQuery 0 0 "cancel"))
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
@ -10,11 +10,12 @@
|
||||||
<D.Content @defaultClass="popup-menu-content is-wide">
|
<D.Content @defaultClass="popup-menu-content is-wide">
|
||||||
<nav class="box menu scroll">
|
<nav class="box menu scroll">
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
{{#each this.months as |month|}}
|
{{#each this.months as |month index|}}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="link"
|
class="button link"
|
||||||
{{on "click" (queue (fn this.selectStartMonth month) (action D.actions.close))}}
|
disabled={{if (lt index this.allowedMonthMax) false true}}
|
||||||
|
{{on "click" (fn this.selectStartMonth month D.actions)}}
|
||||||
>
|
>
|
||||||
{{month}}
|
{{month}}
|
||||||
</button>
|
</button>
|
||||||
|
@ -36,7 +37,12 @@
|
||||||
<nav class="box menu">
|
<nav class="box menu">
|
||||||
<ul class="menu-list">
|
<ul class="menu-list">
|
||||||
{{#each this.years as |year|}}
|
{{#each this.years as |year|}}
|
||||||
<button type="button" class="link" {{on "click" (queue (fn this.selectStartYear year) (action D.actions.close))}}>
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button link"
|
||||||
|
disabled={{if (eq year this.disabledYear) true false}}
|
||||||
|
{{on "click" (fn this.selectStartYear year D.actions)}}
|
||||||
|
>
|
||||||
{{year}}
|
{{year}}
|
||||||
</button>
|
</button>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
@ -50,5 +56,5 @@
|
||||||
disabled={{if (and this.startMonth this.startYear) false true}}
|
disabled={{if (and this.startMonth this.startYear) false true}}
|
||||||
{{on "click" this.saveDateSelection}}
|
{{on "click" this.saveDateSelection}}
|
||||||
>
|
>
|
||||||
Save
|
{{or @submitText "Submit"}}
|
||||||
</button>
|
</button>
|
|
@ -1,15 +0,0 @@
|
||||||
<div class="has-top-margin-xxl">
|
|
||||||
<EmptyState
|
|
||||||
@title={{this.title}}
|
|
||||||
@message={{this.description}}
|
|
||||||
@icon="alert-circle"
|
|
||||||
@bottomBorder={{true}}
|
|
||||||
@subTitle={{join ". " this.auth.mfaErrors}}
|
|
||||||
class="is-box-shadowless"
|
|
||||||
>
|
|
||||||
<button type="button" class="button is-ghost is-transparent" {{on "click" this.onClose}} data-test-go-back>
|
|
||||||
<Icon @name="chevron-left" />
|
|
||||||
Go back
|
|
||||||
</button>
|
|
||||||
</EmptyState>
|
|
||||||
</div>
|
|
|
@ -1,70 +0,0 @@
|
||||||
<div class="auth-form" data-test-mfa-form>
|
|
||||||
<div class="box is-marginless is-shadowless">
|
|
||||||
<p data-test-mfa-description>
|
|
||||||
{{this.description}}
|
|
||||||
</p>
|
|
||||||
<form id="auth-form" {{on "submit" this.submit}}>
|
|
||||||
<MessageError @errors={{this.errors}} class="has-top-margin-s" />
|
|
||||||
<div class="field has-top-margin-l">
|
|
||||||
{{#each this.constraints as |constraint index|}}
|
|
||||||
{{#if index}}
|
|
||||||
<hr />
|
|
||||||
{{/if}}
|
|
||||||
{{#if (gt constraint.methods.length 1)}}
|
|
||||||
<Select
|
|
||||||
@label="Multi-factor authentication method"
|
|
||||||
@options={{constraint.methods}}
|
|
||||||
@valueAttribute={{"id"}}
|
|
||||||
@labelAttribute={{"label"}}
|
|
||||||
@isFullwidth={{true}}
|
|
||||||
@noDefault={{true}}
|
|
||||||
@selectedValue={{constraint.selectedId}}
|
|
||||||
@onChange={{fn this.onSelect constraint}}
|
|
||||||
data-test-mfa-select={{index}}
|
|
||||||
/>
|
|
||||||
{{/if}}
|
|
||||||
{{#if constraint.selectedMethod.uses_passcode}}
|
|
||||||
<label for="passcode" class="is-label" data-test-mfa-passcode-label>
|
|
||||||
{{constraint.selectedMethod.label}}
|
|
||||||
</label>
|
|
||||||
<div class="control">
|
|
||||||
<Input
|
|
||||||
id="passcode"
|
|
||||||
name="passcode"
|
|
||||||
class="input"
|
|
||||||
autocomplete="off"
|
|
||||||
spellcheck="false"
|
|
||||||
autofocus="true"
|
|
||||||
disabled={{or this.validate.isRunning this.newCodeDelay.isRunning}}
|
|
||||||
@value={{constraint.passcode}}
|
|
||||||
data-test-mfa-passcode={{index}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
{{/each}}
|
|
||||||
</div>
|
|
||||||
{{#if this.newCodeDelay.isRunning}}
|
|
||||||
<div>
|
|
||||||
<AlertInline
|
|
||||||
@type="danger"
|
|
||||||
@sizeSmall={{true}}
|
|
||||||
@message="This code is invalid. Please wait until a new code is available."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{{/if}}
|
|
||||||
<button
|
|
||||||
id="validate"
|
|
||||||
type="submit"
|
|
||||||
disabled={{or this.validate.isRunning this.newCodeDelay.isRunning}}
|
|
||||||
class="button is-primary {{if this.validate.isRunning "is-loading"}}"
|
|
||||||
data-test-mfa-validate
|
|
||||||
>
|
|
||||||
Verify
|
|
||||||
</button>
|
|
||||||
{{#if this.newCodeDelay.isRunning}}
|
|
||||||
<Icon @name="delay" class="has-text-grey" />
|
|
||||||
<span class="has-text-grey is-v-centered" data-test-mfa-countdown>{{this.countdown}}</span>
|
|
||||||
{{/if}}
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
|
@ -10,11 +10,7 @@
|
||||||
</div>
|
</div>
|
||||||
</Nav.items>
|
</Nav.items>
|
||||||
</NavHeader>
|
</NavHeader>
|
||||||
{{! bypass UiWizard and container styling }}
|
<UiWizard>
|
||||||
{{#if this.hasAltContent}}
|
|
||||||
{{yield (hash altContent=(component "splash-page/splash-content"))}}
|
|
||||||
{{else}}
|
|
||||||
<UiWizard>
|
|
||||||
<div class="splash-page-container section is-flex-v-centered-tablet is-flex-1 is-fullwidth">
|
<div class="splash-page-container section is-flex-v-centered-tablet is-flex-1 is-fullwidth">
|
||||||
<div class="columns is-centered is-gapless is-fullwidth">
|
<div class="columns is-centered is-gapless is-fullwidth">
|
||||||
<div class="column is-4-desktop is-6-tablet">
|
<div class="column is-4-desktop is-6-tablet">
|
||||||
|
@ -31,5 +27,4 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</UiWizard>
|
</UiWizard>
|
||||||
{{/if}}
|
|
|
@ -10,7 +10,7 @@
|
||||||
<TtlPicker2
|
<TtlPicker2
|
||||||
@initialValue="1h"
|
@initialValue="1h"
|
||||||
@initialEnabled={{false}}
|
@initialEnabled={{false}}
|
||||||
@label="Auto-rotation interval"
|
@label="Auto-rotation period"
|
||||||
@helperTextDisabled="Key will never be automatically rotated"
|
@helperTextDisabled="Key will never be automatically rotated"
|
||||||
@helperTextEnabled="Key will be automatically rotated every"
|
@helperTextEnabled="Key will be automatically rotated every"
|
||||||
@onChange={{@handleAutoRotateChange}}
|
@onChange={{@handleAutoRotateChange}}
|
||||||
|
|
|
@ -18,9 +18,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<TtlPicker2
|
<TtlPicker2
|
||||||
@initialValue={{or @key.autoRotateInterval "1h"}}
|
@initialValue={{or @key.autoRotatePeriod "1h"}}
|
||||||
@initialEnabled={{not (eq @key.autoRotateInterval "0s")}}
|
@initialEnabled={{not (eq @key.autoRotatePeriod "0s")}}
|
||||||
@label="Auto-rotation interval"
|
@label="Auto-rotation period"
|
||||||
@helperTextDisabled="Key will never be automatically rotated"
|
@helperTextDisabled="Key will never be automatically rotated"
|
||||||
@helperTextEnabled="Key will be automatically rotated every"
|
@helperTextEnabled="Key will be automatically rotated every"
|
||||||
@onChange={{@handleAutoRotateChange}}
|
@onChange={{@handleAutoRotateChange}}
|
||||||
|
|
|
@ -171,8 +171,8 @@
|
||||||
{{else}}
|
{{else}}
|
||||||
<InfoTableRow @label="Type" @value={{@key.type}} />
|
<InfoTableRow @label="Type" @value={{@key.type}} />
|
||||||
<InfoTableRow
|
<InfoTableRow
|
||||||
@label="Auto-rotation interval"
|
@label="Auto-rotation period"
|
||||||
@value={{or (format-ttl @key.autoRotateInterval removeZero=true) "Key will not be automatically rotated"}}
|
@value={{or (format-duration @key.autoRotatePeriod removeZero=true) "Key will not be automatically rotated"}}
|
||||||
/>
|
/>
|
||||||
<InfoTableRow @label="Deletion allowed" @value={{stringify @key.deletionAllowed}} />
|
<InfoTableRow @label="Deletion allowed" @value={{stringify @key.deletionAllowed}} />
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,15 @@
|
||||||
<SplashPage @hasAltContent={{this.auth.mfaErrors}} as |Page|>
|
<SplashPage as |Page|>
|
||||||
<Page.altContent>
|
|
||||||
<MfaError @onClose={{fn (mut this.mfaAuthData) null}} />
|
|
||||||
</Page.altContent>
|
|
||||||
<Page.header>
|
<Page.header>
|
||||||
{{#if this.oidcProvider}}
|
{{#if this.oidcProvider}}
|
||||||
<div class="box is-shadowless is-flex-v-centered" data-test-auth-logo>
|
<div class="box is-shadowless is-flex-v-centered" data-test-auth-logo>
|
||||||
<LogoEdition aria-label="Sign in with Hashicorp Vault" />
|
<LogoEdition aria-label="Sign in with Hashicorp Vault" />
|
||||||
</div>
|
</div>
|
||||||
{{else}}
|
{{else}}
|
||||||
<div class="is-flex-row">
|
|
||||||
{{#if this.mfaAuthData}}
|
|
||||||
<button type="button" class="icon-button" {{on "click" (fn (mut this.mfaAuthData) null)}}>
|
|
||||||
<Icon @name="arrow-left" @size="24" aria-label="Back to login" class="icon-blue" />
|
|
||||||
</button>
|
|
||||||
{{/if}}
|
|
||||||
<h1 class="title is-3">
|
<h1 class="title is-3">
|
||||||
{{if this.mfaAuthData "Authenticate" "Sign in to Vault"}}
|
Sign in to Vault
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</Page.header>
|
</Page.header>
|
||||||
{{#unless this.mfaAuthData}}
|
|
||||||
{{#if this.managedNamespaceRoot}}
|
{{#if this.managedNamespaceRoot}}
|
||||||
<Page.sub-header>
|
<Page.sub-header>
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
|
@ -82,20 +71,14 @@
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</Page.sub-header>
|
</Page.sub-header>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/unless}}
|
|
||||||
<Page.content>
|
<Page.content>
|
||||||
{{#if this.mfaAuthData}}
|
|
||||||
<MfaForm @clusterId={{this.model.id}} @authData={{this.mfaAuthData}} @onSuccess={{action "onMfaSuccess"}} />
|
|
||||||
{{else}}
|
|
||||||
<AuthForm
|
<AuthForm
|
||||||
@wrappedToken={{this.wrappedToken}}
|
@wrappedToken={{this.wrappedToken}}
|
||||||
@cluster={{this.model}}
|
@cluster={{this.model}}
|
||||||
@namespace={{this.namespaceQueryParam}}
|
@namespace={{this.namespaceQueryParam}}
|
||||||
@redirectTo={{this.redirectTo}}
|
@redirectTo={{this.redirectTo}}
|
||||||
@selectedAuth={{this.authMethod}}
|
@selectedAuth={{this.authMethod}}
|
||||||
@onSuccess={{action "onAuthResponse"}}
|
|
||||||
/>
|
/>
|
||||||
{{/if}}
|
|
||||||
</Page.content>
|
</Page.content>
|
||||||
<Page.footer>
|
<Page.footer>
|
||||||
<div class="has-short-padding">
|
<div class="has-short-padding">
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
{{#if (eq @model.httpStatus 404)}}
|
||||||
|
<NotFound @model={{this.model}} />
|
||||||
|
{{else}}
|
||||||
|
<EmptyState
|
||||||
|
@title={{if (eq @model.httpStatus 403) "You are not authorized" "Error"}}
|
||||||
|
@subTitle={{concat "Error " @model.httpStatus}}
|
||||||
|
@icon="skip"
|
||||||
|
>
|
||||||
|
{{#if (eq @model.httpStatus 403)}}
|
||||||
|
<p>
|
||||||
|
You must be granted permissions to view this page. Ask your administrator if you think you should have access to the
|
||||||
|
<code>{{@model.path}}</code>
|
||||||
|
endpoint.
|
||||||
|
</p>
|
||||||
|
{{else}}
|
||||||
|
<ul>
|
||||||
|
{{#if @model.message}}
|
||||||
|
<li>{{@model.message}}</li>
|
||||||
|
{{/if}}
|
||||||
|
<hr />
|
||||||
|
{{#each @model.errors as |error|}}
|
||||||
|
<li>
|
||||||
|
{{error}}
|
||||||
|
</li>
|
||||||
|
{{/each}}
|
||||||
|
</ul>
|
||||||
|
{{/if}}
|
||||||
|
</EmptyState>
|
||||||
|
{{/if}}
|
|
@ -10,16 +10,15 @@ import layout from '../templates/components/select';
|
||||||
* <Select @label='Date Range' @options={{[{ value: 'berry', label: 'Berry' }]}} @onChange={{onChange}}/>
|
* <Select @label='Date Range' @options={{[{ value: 'berry', label: 'Berry' }]}} @onChange={{onChange}}/>
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param {string} [label=null] - The label for the select element.
|
* @param label=null {String} - The label for the select element.
|
||||||
* @param {Array} [options=null] - A list of items that the user will select from. This can be an array of strings or objects.
|
* @param options=null {Array} - A list of items that the user will select from. This can be an array of strings or objects.
|
||||||
* @param {string} [selectedValue=null] - The currently selected item. Can also be used to set the default selected item. This should correspond to the `value` of one of the `<option>`s.
|
* @param [selectedValue=null] {String} - The currently selected item. Can also be used to set the default selected item. This should correspond to the `value` of one of the `<option>`s.
|
||||||
* @param {string} [name = null] - The name of the select, used for the test selector.
|
* @param [name=null] {String} - The name of the select, used for the test selector.
|
||||||
* @param {string} [valueAttribute = value]- When `options` is an array objects, the key to check for when assigning the option elements value.
|
* @param [valueAttribute=value] {String} - When `options` is an array objects, the key to check for when assigning the option elements value.
|
||||||
* @param {string} [labelAttribute = label] - When `options` is an array objects, the key to check for when assigning the option elements' inner text.
|
* @param [labelAttribute=label] {String} - When `options` is an array objects, the key to check for when assigning the option elements' inner text.
|
||||||
* @param {boolean} [isInline = false] - Whether or not the select should be displayed as inline-block or block.
|
* @param [isInline=false] {Bool} - Whether or not the select should be displayed as inline-block or block.
|
||||||
* @param {boolean} [isFullwidth = false] - Whether or not the select should take up the full width of the parent element.
|
* @param [isFullwidth=false] {Bool} - Whether or not the select should take up the full width of the parent element.
|
||||||
* @param {boolean} [noDefault = false] - shows Select One with empty value as first option
|
* @param onChange=null {Func} - The action to take once the user has selected an item. This method will be passed the `value` of the select.
|
||||||
* @param {Func} [onChange] - The action to take once the user has selected an item. This method will be passed the `value` of the select.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
|
@ -33,6 +32,5 @@ export default Component.extend({
|
||||||
labelAttribute: 'label',
|
labelAttribute: 'label',
|
||||||
isInline: false,
|
isInline: false,
|
||||||
isFullwidth: false,
|
isFullwidth: false,
|
||||||
noDefault: false,
|
|
||||||
onChange() {},
|
onChange() {},
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,11 +11,6 @@
|
||||||
onchange={{action this.onChange value="target.value"}}
|
onchange={{action this.onChange value="target.value"}}
|
||||||
data-test-select={{this.name}}
|
data-test-select={{this.name}}
|
||||||
>
|
>
|
||||||
{{#if this.noDefault}}
|
|
||||||
<option value="">
|
|
||||||
Select one
|
|
||||||
</option>
|
|
||||||
{{/if}}
|
|
||||||
{{#each this.options as |op|}}
|
{{#each this.options as |op|}}
|
||||||
<option
|
<option
|
||||||
value={{or (get op this.valueAttribute) op}}
|
value={{or (get op this.valueAttribute) op}}
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
import { Factory } from 'ember-cli-mirage';
|
|
||||||
|
|
||||||
export default Factory.extend({
|
|
||||||
type: 'okta',
|
|
||||||
uses_passcode: false,
|
|
||||||
|
|
||||||
afterCreate(mfaMethod) {
|
|
||||||
if (mfaMethod.type === 'totp') {
|
|
||||||
mfaMethod.uses_passcode = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -20,10 +20,15 @@ export default function (server) {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
server.get('sys/internal/counters/config', function (db) {
|
server.get('sys/internal/counters/config', function () {
|
||||||
return {
|
return {
|
||||||
request_id: '00001',
|
request_id: '00001',
|
||||||
data: db['clients/configs'].first(),
|
data: {
|
||||||
|
default_report_months: 12,
|
||||||
|
enabled: 'default-enable',
|
||||||
|
queries_available: true,
|
||||||
|
retention_months: 24,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5573,6 +5578,24 @@ export default function (server) {
|
||||||
non_entity_tokens: 15,
|
non_entity_tokens: 15,
|
||||||
clients: 100,
|
clients: 100,
|
||||||
},
|
},
|
||||||
|
mounts: [
|
||||||
|
{
|
||||||
|
path: 'auth/method/uMGBU',
|
||||||
|
counts: {
|
||||||
|
clients: 35,
|
||||||
|
entity_clients: 20,
|
||||||
|
non_entity_clients: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'auth/method/woiej',
|
||||||
|
counts: {
|
||||||
|
clients: 35,
|
||||||
|
entity_clients: 20,
|
||||||
|
non_entity_clients: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
namespace_id: 'RxD81',
|
namespace_id: 'RxD81',
|
||||||
|
@ -5582,6 +5605,24 @@ export default function (server) {
|
||||||
non_entity_tokens: 20,
|
non_entity_tokens: 20,
|
||||||
clients: 55,
|
clients: 55,
|
||||||
},
|
},
|
||||||
|
mounts: [
|
||||||
|
{
|
||||||
|
path: 'auth/method/ABCD1',
|
||||||
|
counts: {
|
||||||
|
clients: 35,
|
||||||
|
entity_clients: 20,
|
||||||
|
non_entity_clients: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'auth/method/ABCD2',
|
||||||
|
counts: {
|
||||||
|
clients: 35,
|
||||||
|
entity_clients: 20,
|
||||||
|
non_entity_clients: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
namespace_id: 'root',
|
namespace_id: 'root',
|
||||||
|
@ -5591,6 +5632,24 @@ export default function (server) {
|
||||||
non_entity_tokens: 8,
|
non_entity_tokens: 8,
|
||||||
clients: 20,
|
clients: 20,
|
||||||
},
|
},
|
||||||
|
mounts: [
|
||||||
|
{
|
||||||
|
path: 'auth/method/XYZZ2',
|
||||||
|
counts: {
|
||||||
|
clients: 35,
|
||||||
|
entity_clients: 20,
|
||||||
|
non_entity_clients: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'auth/method/XYZZ1',
|
||||||
|
counts: {
|
||||||
|
clients: 35,
|
||||||
|
entity_clients: 20,
|
||||||
|
non_entity_clients: 15,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
distinct_entities: 132,
|
distinct_entities: 132,
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
// add all handlers here
|
// add all handlers here
|
||||||
// individual lookup done in mirage config
|
// individual lookup done in mirage config
|
||||||
import base from './base';
|
import base from './base';
|
||||||
import mfa from './mfa';
|
|
||||||
import activity from './activity';
|
import activity from './activity';
|
||||||
|
|
||||||
export { base, activity, mfa };
|
export { base, activity };
|
||||||
|
|
|
@ -1,146 +0,0 @@
|
||||||
import { Response } from 'miragejs';
|
|
||||||
import Ember from 'ember';
|
|
||||||
import fetch from 'fetch';
|
|
||||||
|
|
||||||
export default function (server) {
|
|
||||||
// initial auth response cache -- lookup by mfa_request_id key
|
|
||||||
const authResponses = {};
|
|
||||||
// mfa requirement cache -- lookup by mfa_request_id key
|
|
||||||
const mfaRequirement = {};
|
|
||||||
// generate different constraint scenarios and return mfa_requirement object
|
|
||||||
const generateMfaRequirement = (req, res) => {
|
|
||||||
const { user } = req.params;
|
|
||||||
// uses_passcode automatically set to true in factory for totp type
|
|
||||||
const m = (type, uses_passcode = false) => server.create('mfa-method', { type, uses_passcode });
|
|
||||||
let mfa_constraints = {};
|
|
||||||
let methods = []; // flat array of methods for easy lookup during validation
|
|
||||||
|
|
||||||
function generator() {
|
|
||||||
const methods = [];
|
|
||||||
const constraintObj = [...arguments].reduce((obj, methodArray, index) => {
|
|
||||||
obj[`test_${index}`] = { any: methodArray };
|
|
||||||
methods.push(...methodArray);
|
|
||||||
return obj;
|
|
||||||
}, {});
|
|
||||||
return [constraintObj, methods];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user === 'mfa-a') {
|
|
||||||
[mfa_constraints, methods] = generator([m('totp')]); // 1 constraint 1 passcode
|
|
||||||
} else if (user === 'mfa-b') {
|
|
||||||
[mfa_constraints, methods] = generator([m('okta')]); // 1 constraint 1 non-passcode
|
|
||||||
} else if (user === 'mfa-c') {
|
|
||||||
[mfa_constraints, methods] = generator([m('totp'), m('duo', true)]); // 1 constraint 2 passcodes
|
|
||||||
} else if (user === 'mfa-d') {
|
|
||||||
[mfa_constraints, methods] = generator([m('okta'), m('duo')]); // 1 constraint 2 non-passcode
|
|
||||||
} else if (user === 'mfa-e') {
|
|
||||||
[mfa_constraints, methods] = generator([m('okta'), m('totp')]); // 1 constraint 1 passcode 1 non-passcode
|
|
||||||
} else if (user === 'mfa-f') {
|
|
||||||
[mfa_constraints, methods] = generator([m('totp')], [m('duo', true)]); // 2 constraints 1 passcode for each
|
|
||||||
} else if (user === 'mfa-g') {
|
|
||||||
[mfa_constraints, methods] = generator([m('okta')], [m('duo')]); // 2 constraints 1 non-passcode for each
|
|
||||||
} else if (user === 'mfa-h') {
|
|
||||||
[mfa_constraints, methods] = generator([m('totp')], [m('okta')]); // 2 constraints 1 passcode 1 non-passcode
|
|
||||||
} else if (user === 'mfa-i') {
|
|
||||||
[mfa_constraints, methods] = generator([m('okta'), m('totp')], [m('totp')]); // 2 constraints 1 passcode/1 non-passcode 1 non-passcode
|
|
||||||
}
|
|
||||||
const numbers = (length) =>
|
|
||||||
Math.random()
|
|
||||||
.toString()
|
|
||||||
.substring(2, length + 2);
|
|
||||||
const mfa_request_id = `${numbers(8)}-${numbers(4)}-${numbers(4)}-${numbers(4)}-${numbers(12)}`;
|
|
||||||
const mfa_requirement = {
|
|
||||||
mfa_request_id,
|
|
||||||
mfa_constraints,
|
|
||||||
};
|
|
||||||
// cache mfa requests to test different validation scenarios
|
|
||||||
mfaRequirement[mfa_request_id] = { methods };
|
|
||||||
// cache auth response to be returned later by sys/mfa/validate
|
|
||||||
authResponses[mfa_request_id] = { ...res };
|
|
||||||
return mfa_requirement;
|
|
||||||
};
|
|
||||||
// passthrough original request, cache response and return mfa stub
|
|
||||||
const passthroughLogin = async (schema, req) => {
|
|
||||||
// test totp not configured scenario
|
|
||||||
if (req.params.user === 'totp-na') {
|
|
||||||
return new Response(400, {}, { errors: ['TOTP mfa required but not configured'] });
|
|
||||||
}
|
|
||||||
const mock = req.params.user ? req.params.user.includes('mfa') : null;
|
|
||||||
// bypass mfa for users that do not match type
|
|
||||||
if (!mock) {
|
|
||||||
req.passthrough();
|
|
||||||
} else if (Ember.testing) {
|
|
||||||
// use root token in test environment
|
|
||||||
const res = await fetch('/v1/auth/token/lookup-self', { headers: { 'X-Vault-Token': 'root' } });
|
|
||||||
if (res.status < 300) {
|
|
||||||
const json = res.json();
|
|
||||||
if (Ember.testing) {
|
|
||||||
json.auth = {
|
|
||||||
...json.data,
|
|
||||||
policies: [],
|
|
||||||
metadata: { username: 'foobar' },
|
|
||||||
};
|
|
||||||
json.data = null;
|
|
||||||
}
|
|
||||||
return { auth: { mfa_requirement: generateMfaRequirement(req, json) } };
|
|
||||||
}
|
|
||||||
return new Response(500, {}, { errors: ['Mirage error fetching root token in testing'] });
|
|
||||||
} else {
|
|
||||||
const xhr = req.passthrough();
|
|
||||||
xhr.onreadystatechange = () => {
|
|
||||||
if (xhr.readyState === 4 && xhr.status < 300) {
|
|
||||||
// XMLHttpRequest response prop only has a getter -- redefine as writable and set value
|
|
||||||
Object.defineProperty(xhr, 'response', {
|
|
||||||
writable: true,
|
|
||||||
value: JSON.stringify({
|
|
||||||
auth: { mfa_requirement: generateMfaRequirement(req, JSON.parse(xhr.responseText)) },
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
server.post('/auth/:method/login/:user', passthroughLogin);
|
|
||||||
|
|
||||||
server.post('/sys/mfa/validate', (schema, req) => {
|
|
||||||
try {
|
|
||||||
const { mfa_request_id, mfa_payload } = JSON.parse(req.requestBody);
|
|
||||||
const mfaRequest = mfaRequirement[mfa_request_id];
|
|
||||||
|
|
||||||
if (!mfaRequest) {
|
|
||||||
return new Response(404, {}, { errors: ['MFA Request ID not found'] });
|
|
||||||
}
|
|
||||||
// validate request body
|
|
||||||
for (let constraintId in mfa_payload) {
|
|
||||||
// ensure ids were passed in map
|
|
||||||
const method = mfaRequest.methods.find(({ id }) => id === constraintId);
|
|
||||||
if (!method) {
|
|
||||||
return new Response(
|
|
||||||
400,
|
|
||||||
{},
|
|
||||||
{ errors: [`Invalid MFA constraint id ${constraintId} passed in map`] }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// test non-totp validation by rejecting all pingid requests
|
|
||||||
if (method.type === 'pingid') {
|
|
||||||
return new Response(403, {}, { errors: ['PingId MFA validation failed'] });
|
|
||||||
}
|
|
||||||
// validate totp passcode
|
|
||||||
const passcode = mfa_payload[constraintId][0];
|
|
||||||
if (method.uses_passcode) {
|
|
||||||
if (passcode !== 'test') {
|
|
||||||
const error = !passcode ? 'TOTP passcode not provided' : 'Incorrect TOTP passcode provided';
|
|
||||||
return new Response(403, {}, { errors: [error] });
|
|
||||||
}
|
|
||||||
} else if (passcode) {
|
|
||||||
// for okta and duo, reject if a passcode was provided
|
|
||||||
return new Response(400, {}, { errors: ['Passcode should only be provided for TOTP MFA type'] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return authResponses[mfa_request_id];
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
return new Response(500, {}, { errors: ['Mirage Handler Error: /sys/mfa/validate'] });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -3,21 +3,25 @@
|
||||||
## AuthForm
|
## AuthForm
|
||||||
The `AuthForm` is used to sign users into Vault.
|
The `AuthForm` is used to sign users into Vault.
|
||||||
|
|
||||||
**Params**
|
|
||||||
|
|
||||||
| Param | Type | Description |
|
| Param | Type | Default | Description |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| wrappedToken | <code>string</code> | The auth method that is currently selected in the dropdown. |
|
| wrappedToken | <code>String</code> | <code></code> | The auth method that is currently selected in the dropdown. |
|
||||||
| cluster | <code>object</code> | The auth method that is currently selected in the dropdown. This corresponds to an Ember Model. |
|
| cluster | <code>Object</code> | <code></code> | The auth method that is currently selected in the dropdown. This corresponds to an Ember Model. |
|
||||||
| namespace- | <code>string</code> | The currently active namespace. |
|
| namespace | <code>String</code> | <code></code> | The currently active namespace. |
|
||||||
| selectedAuth | <code>string</code> | The auth method that is currently selected in the dropdown. |
|
| redirectTo | <code>String</code> | <code></code> | The name of the route to redirect to. |
|
||||||
| onSuccess | <code>function</code> | Fired on auth success |
|
| selectedAuth | <code>String</code> | <code></code> | The auth method that is currently selected in the dropdown. |
|
||||||
|
|
||||||
**Example**
|
**Example**
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// All properties are passed in via query params.
|
// All properties are passed in via query params.
|
||||||
<AuthForm @wrappedToken={{wrappedToken}} @cluster={{model}} @namespace={{namespaceQueryParam}} @selectedAuth={{authMethod}} @onSuccess={{action this.onSuccess}} />```
|
<AuthForm
|
||||||
|
@wrappedToken={{wrappedToken}}
|
||||||
|
@cluster={{model}}
|
||||||
|
@namespace={{namespaceQueryParam}}
|
||||||
|
@redirectTo={{redirectTo}}
|
||||||
|
@selectedAuth={{authMethod}}/>```
|
||||||
|
|
||||||
**See**
|
**See**
|
||||||
|
|
||||||
|
|
|
@ -110,10 +110,16 @@ module('Acceptance | auth', function (hooks) {
|
||||||
assert.dom('[data-test-allow-expiration]').doesNotExist('hides beacon when the api is used again');
|
assert.dom('[data-test-allow-expiration]').doesNotExist('hides beacon when the api is used again');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it shows the push notification warning after submit', async function (assert) {
|
test('it shows the push notification warning only for okta auth method after submit', async function (assert) {
|
||||||
await visit('/vault/auth');
|
await visit('/vault/auth');
|
||||||
await component.selectMethod('token');
|
await component.selectMethod('token');
|
||||||
await click('[data-test-auth-submit]');
|
await click('[data-test-auth-submit]');
|
||||||
|
assert
|
||||||
|
.dom('[data-test-auth-message="push"]')
|
||||||
|
.doesNotExist('message is not shown for other authentication methods');
|
||||||
|
|
||||||
|
await component.selectMethod('okta');
|
||||||
|
await click('[data-test-auth-submit]');
|
||||||
assert.dom('[data-test-auth-message="push"]').exists('shows push notification message');
|
assert.dom('[data-test-auth-message="push"]').exists('shows push notification message');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,135 +0,0 @@
|
||||||
import { module, test } from 'qunit';
|
|
||||||
import { setupApplicationTest } from 'ember-qunit';
|
|
||||||
import { click, currentRouteName, fillIn, visit } from '@ember/test-helpers';
|
|
||||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
|
||||||
import ENV from 'vault/config/environment';
|
|
||||||
|
|
||||||
ENV['ember-cli-mirage'].handler = 'mfa';
|
|
||||||
|
|
||||||
module('Acceptance | mfa', function (hooks) {
|
|
||||||
setupApplicationTest(hooks);
|
|
||||||
setupMirage(hooks);
|
|
||||||
|
|
||||||
hooks.beforeEach(function () {
|
|
||||||
this.select = async (select = 0, option = 1) => {
|
|
||||||
const selector = `[data-test-mfa-select="${select}"]`;
|
|
||||||
const value = this.element.querySelector(`${selector} option:nth-child(${option + 1})`).value;
|
|
||||||
await fillIn(`${selector} select`, value);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const login = async (user) => {
|
|
||||||
// MfaHandler(server);
|
|
||||||
await visit('/vault/auth');
|
|
||||||
await fillIn('[data-test-select="auth-method"]', 'userpass');
|
|
||||||
await fillIn('[data-test-username]', user);
|
|
||||||
await fillIn('[data-test-password]', 'test');
|
|
||||||
await click('[data-test-auth-submit]');
|
|
||||||
};
|
|
||||||
const didLogin = (assert) => {
|
|
||||||
assert.equal(currentRouteName(), 'vault.cluster.secrets.backends', 'Route transitions after login');
|
|
||||||
};
|
|
||||||
const validate = async (multi) => {
|
|
||||||
await fillIn('[data-test-mfa-passcode="0"]', 'test');
|
|
||||||
if (multi) {
|
|
||||||
await fillIn('[data-test-mfa-passcode="1"]', 'test');
|
|
||||||
}
|
|
||||||
await click('[data-test-mfa-validate]');
|
|
||||||
};
|
|
||||||
|
|
||||||
test('it should handle single mfa constraint with passcode method', async function (assert) {
|
|
||||||
await login('mfa-a');
|
|
||||||
assert
|
|
||||||
.dom('[data-test-mfa-description]')
|
|
||||||
.includesText(
|
|
||||||
'Enter your authentication code to log in.',
|
|
||||||
'Mfa form displays with correct description'
|
|
||||||
);
|
|
||||||
assert.dom('[data-test-mfa-select]').doesNotExist('Select is hidden for single method');
|
|
||||||
assert.dom('[data-test-mfa-passcode]').exists({ count: 1 }, 'Single passcode input renders');
|
|
||||||
await validate();
|
|
||||||
didLogin(assert);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it should handle single mfa constraint with push method', async function (assert) {
|
|
||||||
await login('mfa-b');
|
|
||||||
didLogin(assert);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it should handle single mfa constraint with 2 passcode methods', async function (assert) {
|
|
||||||
await login('mfa-c');
|
|
||||||
assert
|
|
||||||
.dom('[data-test-mfa-description]')
|
|
||||||
.includesText('Select the MFA method you wish to use.', 'Mfa form displays with correct description');
|
|
||||||
assert
|
|
||||||
.dom('[data-test-mfa-select]')
|
|
||||||
.exists({ count: 1 }, 'Select renders for single constraint with multiple methods');
|
|
||||||
assert.dom('[data-test-mfa-passcode]').doesNotExist('Passcode input hidden until selection is made');
|
|
||||||
await this.select();
|
|
||||||
await validate();
|
|
||||||
didLogin(assert);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it should handle single mfa constraint with 2 push methods', async function (assert) {
|
|
||||||
await login('mfa-d');
|
|
||||||
await this.select();
|
|
||||||
await click('[data-test-mfa-validate]');
|
|
||||||
didLogin(assert);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it should handle single mfa constraint with 1 passcode and 1 push method', async function (assert) {
|
|
||||||
await login('mfa-e');
|
|
||||||
await this.select(0, 2);
|
|
||||||
assert.dom('[data-test-mfa-passcode]').exists('Passcode input renders');
|
|
||||||
await this.select();
|
|
||||||
assert.dom('[data-test-mfa-passcode]').doesNotExist('Passcode input is hidden for push method');
|
|
||||||
await click('[data-test-mfa-validate]');
|
|
||||||
didLogin(assert);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it should handle multiple mfa constraints with 1 passcode method each', async function (assert) {
|
|
||||||
await login('mfa-f');
|
|
||||||
assert
|
|
||||||
.dom('[data-test-mfa-description]')
|
|
||||||
.includesText(
|
|
||||||
'Two methods are required for successful authentication.',
|
|
||||||
'Mfa form displays with correct description'
|
|
||||||
);
|
|
||||||
assert.dom('[data-test-mfa-select]').doesNotExist('Selects do not render for single methods');
|
|
||||||
await validate(true);
|
|
||||||
didLogin(assert);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it should handle multi mfa constraint with 1 push method each', async function (assert) {
|
|
||||||
await login('mfa-g');
|
|
||||||
didLogin(assert);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it should handle multiple mfa constraints with 1 passcode and 1 push method', async function (assert) {
|
|
||||||
await login('mfa-h');
|
|
||||||
assert
|
|
||||||
.dom('[data-test-mfa-description]')
|
|
||||||
.includesText(
|
|
||||||
'Two methods are required for successful authentication.',
|
|
||||||
'Mfa form displays with correct description'
|
|
||||||
);
|
|
||||||
assert.dom('[data-test-mfa-select]').doesNotExist('Select is hidden for single method');
|
|
||||||
assert.dom('[data-test-mfa-passcode]').exists({ count: 1 }, 'Passcode input renders');
|
|
||||||
await validate();
|
|
||||||
didLogin(assert);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it should handle multiple mfa constraints with multiple mixed methods', async function (assert) {
|
|
||||||
await login('mfa-i');
|
|
||||||
assert
|
|
||||||
.dom('[data-test-mfa-description]')
|
|
||||||
.includesText(
|
|
||||||
'Two methods are required for successful authentication.',
|
|
||||||
'Mfa form displays with correct description'
|
|
||||||
);
|
|
||||||
await this.select();
|
|
||||||
await fillIn('[data-test-mfa-passcode="1"]', 'test');
|
|
||||||
await click('[data-test-mfa-validate]');
|
|
||||||
didLogin(assert);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -18,7 +18,6 @@ const authService = Service.extend({
|
||||||
async authenticate() {
|
async authenticate() {
|
||||||
return fetch('http://localhost:2000');
|
return fetch('http://localhost:2000');
|
||||||
},
|
},
|
||||||
handleError() {},
|
|
||||||
setLastFetch() {},
|
setLastFetch() {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -26,7 +25,6 @@ const workingAuthService = Service.extend({
|
||||||
authenticate() {
|
authenticate() {
|
||||||
return resolve({});
|
return resolve({});
|
||||||
},
|
},
|
||||||
handleError() {},
|
|
||||||
setLastFetch() {},
|
setLastFetch() {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
import { module, test } from 'qunit';
|
|
||||||
import { setupRenderingTest } from 'ember-qunit';
|
|
||||||
import { render } from '@ember/test-helpers';
|
|
||||||
import { hbs } from 'ember-cli-htmlbars';
|
|
||||||
import { click } from '@ember/test-helpers';
|
|
||||||
import { TOTP_NOT_CONFIGURED } from 'vault/services/auth';
|
|
||||||
import { TOTP_NA_MSG, MFA_ERROR_MSG } from 'vault/components/mfa-error';
|
|
||||||
const UNAUTH = 'MFA authorization failed';
|
|
||||||
|
|
||||||
module('Integration | Component | mfa-error', function (hooks) {
|
|
||||||
setupRenderingTest(hooks);
|
|
||||||
|
|
||||||
test('it renders', async function (assert) {
|
|
||||||
const auth = this.owner.lookup('service:auth');
|
|
||||||
auth.set('mfaErrors', [TOTP_NOT_CONFIGURED]);
|
|
||||||
|
|
||||||
this.onClose = () => assert.ok(true, 'onClose event is triggered');
|
|
||||||
|
|
||||||
await render(hbs`<MfaError @onClose={{this.onClose}}/>`);
|
|
||||||
|
|
||||||
assert.dom('[data-test-empty-state-title]').hasText('TOTP not set up', 'Title renders for TOTP error');
|
|
||||||
assert
|
|
||||||
.dom('[data-test-empty-state-subText]')
|
|
||||||
.hasText(TOTP_NOT_CONFIGURED, 'Error message renders for TOTP error');
|
|
||||||
assert.dom('[data-test-empty-state-message]').hasText(TOTP_NA_MSG, 'Description renders for TOTP error');
|
|
||||||
|
|
||||||
auth.set('mfaErrors', [UNAUTH]);
|
|
||||||
await render(hbs`<MfaError @onClose={{this.onClose}}/>`);
|
|
||||||
|
|
||||||
assert.dom('[data-test-empty-state-title]').hasText('Unauthorized', 'Title renders for mfa error');
|
|
||||||
assert.dom('[data-test-empty-state-subText]').hasText(UNAUTH, 'Error message renders for mfa error');
|
|
||||||
assert.dom('[data-test-empty-state-message]').hasText(MFA_ERROR_MSG, 'Description renders for mfa error');
|
|
||||||
|
|
||||||
await click('[data-test-go-back]');
|
|
||||||
|
|
||||||
assert.equal(auth.mfaErrors, null, 'mfaErrors unset in auth service');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,190 +0,0 @@
|
||||||
import { module, test, skip } from 'qunit';
|
|
||||||
import { setupRenderingTest } from 'ember-qunit';
|
|
||||||
import { render } from '@ember/test-helpers';
|
|
||||||
import { hbs } from 'ember-cli-htmlbars';
|
|
||||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
|
||||||
import { fillIn, click, waitUntil } from '@ember/test-helpers';
|
|
||||||
import { run, later } from '@ember/runloop';
|
|
||||||
|
|
||||||
module('Integration | Component | mfa-form', function (hooks) {
|
|
||||||
setupRenderingTest(hooks);
|
|
||||||
setupMirage(hooks);
|
|
||||||
|
|
||||||
hooks.beforeEach(function () {
|
|
||||||
this.clusterId = '123456';
|
|
||||||
this.mfaAuthData = {
|
|
||||||
backend: 'userpass',
|
|
||||||
data: { username: 'foo', password: 'bar' },
|
|
||||||
};
|
|
||||||
this.authService = this.owner.lookup('service:auth');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it should render correct descriptions', async function (assert) {
|
|
||||||
const totpConstraint = this.server.create('mfa-method', { type: 'totp' });
|
|
||||||
const oktaConstraint = this.server.create('mfa-method', { type: 'okta' });
|
|
||||||
const duoConstraint = this.server.create('mfa-method', { type: 'duo' });
|
|
||||||
|
|
||||||
this.mfaAuthData.mfa_requirement = this.authService._parseMfaResponse({
|
|
||||||
mfa_request_id: 'test-mfa-id',
|
|
||||||
mfa_constraints: { test_mfa_1: { any: [totpConstraint] } },
|
|
||||||
}).mfa_requirement;
|
|
||||||
|
|
||||||
await render(hbs`<MfaForm @clusterId={{this.clusterId}} @authData={{this.mfaAuthData}} />`);
|
|
||||||
assert
|
|
||||||
.dom('[data-test-mfa-description]')
|
|
||||||
.includesText(
|
|
||||||
'Enter your authentication code to log in.',
|
|
||||||
'Correct description renders for single passcode'
|
|
||||||
);
|
|
||||||
|
|
||||||
this.mfaAuthData.mfa_requirement = this.authService._parseMfaResponse({
|
|
||||||
mfa_request_id: 'test-mfa-id',
|
|
||||||
mfa_constraints: { test_mfa_1: { any: [duoConstraint, oktaConstraint] } },
|
|
||||||
}).mfa_requirement;
|
|
||||||
|
|
||||||
await render(hbs`<MfaForm @clusterId={{this.clusterId}} @authData={{this.mfaAuthData}} />`);
|
|
||||||
assert
|
|
||||||
.dom('[data-test-mfa-description]')
|
|
||||||
.includesText(
|
|
||||||
'Select the MFA method you wish to use.',
|
|
||||||
'Correct description renders for multiple methods'
|
|
||||||
);
|
|
||||||
|
|
||||||
this.mfaAuthData.mfa_requirement = this.authService._parseMfaResponse({
|
|
||||||
mfa_request_id: 'test-mfa-id',
|
|
||||||
mfa_constraints: { test_mfa_1: { any: [oktaConstraint] }, test_mfa_2: { any: [duoConstraint] } },
|
|
||||||
}).mfa_requirement;
|
|
||||||
|
|
||||||
await render(hbs`<MfaForm @clusterId={{this.clusterId}} @authData={{this.mfaAuthData}} />`);
|
|
||||||
assert
|
|
||||||
.dom('[data-test-mfa-description]')
|
|
||||||
.includesText(
|
|
||||||
'Two methods are required for successful authentication.',
|
|
||||||
'Correct description renders for multiple constraints'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it should render method selects and passcode inputs', async function (assert) {
|
|
||||||
const duoConstraint = this.server.create('mfa-method', { type: 'duo', uses_passcode: true });
|
|
||||||
const oktaConstraint = this.server.create('mfa-method', { type: 'okta' });
|
|
||||||
const pingidConstraint = this.server.create('mfa-method', { type: 'pingid' });
|
|
||||||
const { mfa_requirement } = this.authService._parseMfaResponse({
|
|
||||||
mfa_request_id: 'test-mfa-id',
|
|
||||||
mfa_constraints: {
|
|
||||||
test_mfa_1: {
|
|
||||||
any: [pingidConstraint, oktaConstraint],
|
|
||||||
},
|
|
||||||
test_mfa_2: {
|
|
||||||
any: [duoConstraint],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.mfaAuthData.mfa_requirement = mfa_requirement;
|
|
||||||
|
|
||||||
this.server.post('/sys/mfa/validate', (schema, req) => {
|
|
||||||
const json = JSON.parse(req.requestBody);
|
|
||||||
const payload = {
|
|
||||||
mfa_request_id: 'test-mfa-id',
|
|
||||||
mfa_payload: { [oktaConstraint.id]: [], [duoConstraint.id]: ['test-code'] },
|
|
||||||
};
|
|
||||||
assert.deepEqual(json, payload, 'Correct mfa payload passed to validate endpoint');
|
|
||||||
return {};
|
|
||||||
});
|
|
||||||
|
|
||||||
this.owner.lookup('service:auth').reopen({
|
|
||||||
// override to avoid authSuccess method since it expects an auth payload
|
|
||||||
async totpValidate({ mfa_requirement }) {
|
|
||||||
await this.clusterAdapter().mfaValidate(mfa_requirement);
|
|
||||||
return 'test response';
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.onSuccess = (resp) =>
|
|
||||||
assert.equal(resp, 'test response', 'Response is returned in onSuccess callback');
|
|
||||||
|
|
||||||
await render(hbs`
|
|
||||||
<MfaForm
|
|
||||||
@clusterId={{this.clusterId}}
|
|
||||||
@authData={{this.mfaAuthData}}
|
|
||||||
@onSuccess={{this.onSuccess}}
|
|
||||||
/>
|
|
||||||
`);
|
|
||||||
await fillIn('[data-test-mfa-select="0"] select', oktaConstraint.id);
|
|
||||||
await fillIn('[data-test-mfa-passcode="1"]', 'test-code');
|
|
||||||
await click('[data-test-mfa-validate]');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it should validate mfa requirement', async function (assert) {
|
|
||||||
const totpConstraint = this.server.create('mfa-method', { type: 'totp' });
|
|
||||||
const { mfa_requirement } = this.authService._parseMfaResponse({
|
|
||||||
mfa_request_id: 'test-mfa-id',
|
|
||||||
mfa_constraints: {
|
|
||||||
test_mfa: {
|
|
||||||
any: [totpConstraint],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.mfaAuthData.mfa_requirement = mfa_requirement;
|
|
||||||
|
|
||||||
this.server.post('/sys/mfa/validate', (schema, req) => {
|
|
||||||
const json = JSON.parse(req.requestBody);
|
|
||||||
const payload = {
|
|
||||||
mfa_request_id: 'test-mfa-id',
|
|
||||||
mfa_payload: { [totpConstraint.id]: ['test-code'] },
|
|
||||||
};
|
|
||||||
assert.deepEqual(json, payload, 'Correct mfa payload passed to validate endpoint');
|
|
||||||
return {};
|
|
||||||
});
|
|
||||||
|
|
||||||
const expectedAuthData = { clusterId: this.clusterId, ...this.mfaAuthData };
|
|
||||||
this.owner.lookup('service:auth').reopen({
|
|
||||||
// override to avoid authSuccess method since it expects an auth payload
|
|
||||||
async totpValidate(authData) {
|
|
||||||
await waitUntil(() =>
|
|
||||||
assert.dom('[data-test-mfa-validate]').hasClass('is-loading', 'Loading class applied to button')
|
|
||||||
);
|
|
||||||
assert.dom('[data-test-mfa-validate]').isDisabled('Button is disabled while loading');
|
|
||||||
assert.deepEqual(authData, expectedAuthData, 'Mfa auth data passed to validate method');
|
|
||||||
await this.clusterAdapter().mfaValidate(authData.mfa_requirement);
|
|
||||||
return 'test response';
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.onSuccess = (resp) =>
|
|
||||||
assert.equal(resp, 'test response', 'Response is returned in onSuccess callback');
|
|
||||||
|
|
||||||
await render(hbs`
|
|
||||||
<MfaForm
|
|
||||||
@clusterId={{this.clusterId}}
|
|
||||||
@authData={{this.mfaAuthData}}
|
|
||||||
@onSuccess={{this.onSuccess}}
|
|
||||||
/>
|
|
||||||
`);
|
|
||||||
await fillIn('[data-test-mfa-passcode]', 'test-code');
|
|
||||||
await click('[data-test-mfa-validate]');
|
|
||||||
});
|
|
||||||
|
|
||||||
// commented out in component until specific error code can be parsed from the api response
|
|
||||||
skip('it should show countdown on passcode validation failure', async function (assert) {
|
|
||||||
this.owner.lookup('service:auth').reopen({
|
|
||||||
totpValidate() {
|
|
||||||
throw new Error('Incorrect passcode');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
await render(hbs`
|
|
||||||
<MfaForm
|
|
||||||
@clusterId={{this.clusterId}}
|
|
||||||
@authData={{this.mfaAuthData}}
|
|
||||||
/>
|
|
||||||
`);
|
|
||||||
|
|
||||||
await fillIn('[data-test-mfa-passcode]', 'test-code');
|
|
||||||
later(() => run.cancelTimers(), 50);
|
|
||||||
await click('[data-test-mfa-validate]');
|
|
||||||
assert.dom('[data-test-mfa-validate]').isDisabled('Button is disabled during countdown');
|
|
||||||
assert.dom('[data-test-mfa-passcode]').isDisabled('Input is disabled during countdown');
|
|
||||||
assert.dom('[data-test-mfa-passcode]').hasNoValue('Input value is cleared on error');
|
|
||||||
assert.dom('[data-test-inline-error-message]').exists('Alert message renders');
|
|
||||||
assert.dom('[data-test-mfa-countdown]').exists('30 second countdown renders');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -6,6 +6,14 @@ import { hbs } from 'ember-cli-htmlbars';
|
||||||
module('Integration | Helper | format-ttl', function (hooks) {
|
module('Integration | Helper | format-ttl', function (hooks) {
|
||||||
setupRenderingTest(hooks);
|
setupRenderingTest(hooks);
|
||||||
|
|
||||||
|
test('it does not fail if no input', async function (assert) {
|
||||||
|
this.set('inputValue', '');
|
||||||
|
|
||||||
|
await render(hbs`{{format-ttl inputValue}}`);
|
||||||
|
|
||||||
|
assert.equal(this.element.textContent.trim(), '');
|
||||||
|
});
|
||||||
|
|
||||||
test('it renders the input if no match found', async function (assert) {
|
test('it renders the input if no match found', async function (assert) {
|
||||||
this.set('inputValue', '1234');
|
this.set('inputValue', '1234');
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,6 @@ export default create({
|
||||||
// make sure we're always logged out and logged back in
|
// make sure we're always logged out and logged back in
|
||||||
await this.logout();
|
await this.logout();
|
||||||
await settled();
|
await settled();
|
||||||
// clear local storage to ensure we have a clean state
|
|
||||||
window.localStorage.clear();
|
|
||||||
await this.visit({ with: 'token' });
|
await this.visit({ with: 'token' });
|
||||||
await settled();
|
await settled();
|
||||||
if (token) {
|
if (token) {
|
||||||
|
|
|
@ -424,10 +424,15 @@ func (c *Core) taintCredEntry(ctx context.Context, path string, updateStorage bo
|
||||||
c.authLock.Lock()
|
c.authLock.Lock()
|
||||||
defer c.authLock.Unlock()
|
defer c.authLock.Unlock()
|
||||||
|
|
||||||
|
ns, err := namespace.FromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Taint the entry from the auth table
|
// Taint the entry from the auth table
|
||||||
// We do this on the original since setting the taint operates
|
// We do this on the original since setting the taint operates
|
||||||
// on the entries which a shallow clone shares anyways
|
// on the entries which a shallow clone shares anyways
|
||||||
entry, err := c.auth.setTaint(ctx, strings.TrimPrefix(path, credentialRoutePrefix), true, mountStateUnmounting)
|
entry, err := c.auth.setTaint(ns.ID, strings.TrimPrefix(path, credentialRoutePrefix), true, mountStateUnmounting)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -311,6 +311,10 @@ type Core struct {
|
||||||
// change underneath a calling function
|
// change underneath a calling function
|
||||||
mountsLock sync.RWMutex
|
mountsLock sync.RWMutex
|
||||||
|
|
||||||
|
// mountMigrationTracker tracks past and ongoing remount operations
|
||||||
|
// against their migration ids
|
||||||
|
mountMigrationTracker *sync.Map
|
||||||
|
|
||||||
// auth is loaded after unseal since it is a protected
|
// auth is loaded after unseal since it is a protected
|
||||||
// configuration
|
// configuration
|
||||||
auth *MountTable
|
auth *MountTable
|
||||||
|
@ -855,6 +859,7 @@ func CreateCore(conf *CoreConfig) (*Core, error) {
|
||||||
disableAutopilot: conf.DisableAutopilot,
|
disableAutopilot: conf.DisableAutopilot,
|
||||||
enableResponseHeaderHostname: conf.EnableResponseHeaderHostname,
|
enableResponseHeaderHostname: conf.EnableResponseHeaderHostname,
|
||||||
enableResponseHeaderRaftNodeID: conf.EnableResponseHeaderRaftNodeID,
|
enableResponseHeaderRaftNodeID: conf.EnableResponseHeaderRaftNodeID,
|
||||||
|
mountMigrationTracker: &sync.Map{},
|
||||||
disableSSCTokens: conf.DisableSSCTokens,
|
disableSSCTokens: conf.DisableSSCTokens,
|
||||||
}
|
}
|
||||||
c.standbyStopCh.Store(make(chan struct{}))
|
c.standbyStopCh.Store(make(chan struct{}))
|
||||||
|
|
|
@ -178,7 +178,7 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend {
|
||||||
b.Backend.Paths = append(b.Backend.Paths, b.capabilitiesPaths()...)
|
b.Backend.Paths = append(b.Backend.Paths, b.capabilitiesPaths()...)
|
||||||
b.Backend.Paths = append(b.Backend.Paths, b.internalPaths()...)
|
b.Backend.Paths = append(b.Backend.Paths, b.internalPaths()...)
|
||||||
b.Backend.Paths = append(b.Backend.Paths, b.pprofPaths()...)
|
b.Backend.Paths = append(b.Backend.Paths, b.pprofPaths()...)
|
||||||
b.Backend.Paths = append(b.Backend.Paths, b.remountPath())
|
b.Backend.Paths = append(b.Backend.Paths, b.remountPaths()...)
|
||||||
b.Backend.Paths = append(b.Backend.Paths, b.metricsPath())
|
b.Backend.Paths = append(b.Backend.Paths, b.metricsPath())
|
||||||
b.Backend.Paths = append(b.Backend.Paths, b.monitorPath())
|
b.Backend.Paths = append(b.Backend.Paths, b.monitorPath())
|
||||||
b.Backend.Paths = append(b.Backend.Paths, b.inFlightRequestPath())
|
b.Backend.Paths = append(b.Backend.Paths, b.inFlightRequestPath())
|
||||||
|
@ -1199,11 +1199,33 @@ func (b *SystemBackend) handleRemount(ctx context.Context, req *logical.Request,
|
||||||
logical.ErrInvalidRequest
|
logical.ErrInvalidRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = validateMountPath(toPath); err != nil {
|
fromPathDetails := b.Core.splitNamespaceAndMountFromPath(ns.Path, fromPath)
|
||||||
return handleError(fmt.Errorf("'to' %v", err))
|
toPathDetails := b.Core.splitNamespaceAndMountFromPath(ns.Path, toPath)
|
||||||
|
|
||||||
|
if err = validateMountPath(toPathDetails.MountPath); err != nil {
|
||||||
|
return handleError(fmt.Errorf("invalid destination mount: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
entry := b.Core.router.MatchingMountEntry(ctx, fromPath)
|
// Prevent target and source mounts from being in a protected path
|
||||||
|
for _, p := range protectedMounts {
|
||||||
|
if strings.HasPrefix(fromPathDetails.MountPath, p) {
|
||||||
|
return handleError(fmt.Errorf("cannot remount %q", fromPathDetails.MountPath))
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(toPathDetails.MountPath, p) {
|
||||||
|
return handleError(fmt.Errorf("cannot remount to destination %+v", toPathDetails.MountPath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := b.Core.router.MatchingMountEntry(ctx, sanitizePath(fromPath))
|
||||||
|
|
||||||
|
if entry == nil {
|
||||||
|
return handleError(fmt.Errorf("no matching mount at %q", sanitizePath(fromPath)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if match := b.Core.router.MatchingMount(ctx, toPath); match != "" {
|
||||||
|
return handleError(fmt.Errorf("existing mount at %q", match))
|
||||||
|
}
|
||||||
// If we are a performance secondary cluster we should forward the request
|
// If we are a performance secondary cluster we should forward the request
|
||||||
// to the primary. We fail early here since the view in use isn't marked as
|
// to the primary. We fail early here since the view in use isn't marked as
|
||||||
// readonly
|
// readonly
|
||||||
|
@ -1211,31 +1233,76 @@ func (b *SystemBackend) handleRemount(ctx context.Context, req *logical.Request,
|
||||||
return nil, logical.ErrReadOnly
|
return nil, logical.ErrReadOnly
|
||||||
}
|
}
|
||||||
|
|
||||||
|
migrationID, err := b.Core.createMigrationStatus(fromPathDetails, toPathDetails)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error creating migration status %+v", err)
|
||||||
|
}
|
||||||
|
// Start up a goroutine to handle the remount operations, and return early to the caller
|
||||||
|
go func(migrationID string) {
|
||||||
|
b.Core.stateLock.RLock()
|
||||||
|
defer b.Core.stateLock.RUnlock()
|
||||||
|
|
||||||
|
logger := b.Core.Logger().Named("mounts.migration").With("migration_id", migrationID, "namespace", ns.Path, "to_path", toPath, "from_path", fromPath)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if !strings.Contains(fromPath, "auth") {
|
||||||
|
err = b.moveSecretsEngine(ns, logger, migrationID, entry.ViewPath(), fromPathDetails, toPathDetails)
|
||||||
|
} else {
|
||||||
|
logger.Error("Remount is unsupported for the source mount", "err", err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("remount failed", "error", err)
|
||||||
|
if err := b.Core.setMigrationStatus(migrationID, MigrationFailureStatus); err != nil {
|
||||||
|
logger.Error("Setting migration status failed", "error", err, "target_status", MigrationFailureStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(migrationID)
|
||||||
|
|
||||||
|
resp := &logical.Response{
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"migration_id": migrationID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resp.AddWarning("Mount move has been queued. Progress will be reported in Vault's server log, tagged with the returned migration_id")
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// moveSecretsEngine carries out a remount operation on the secrets engine, updating the migration status as required
|
||||||
|
// It is expected to be called asynchronously outside of a request context, hence it creates a context derived from the active one
|
||||||
|
// and intermittently checks to see if it is still open.
|
||||||
|
func (b *SystemBackend) moveSecretsEngine(ns *namespace.Namespace, logger log.Logger, migrationID, viewPath string, fromPathDetails, toPathDetails namespace.MountPathDetails) error {
|
||||||
|
logger.Info("Starting to update the mount table and revoke leases")
|
||||||
|
revokeCtx := namespace.ContextWithNamespace(b.Core.activeContext, ns)
|
||||||
// Attempt remount
|
// Attempt remount
|
||||||
if err := b.Core.remount(ctx, fromPath, toPath, !b.Core.perfStandby); err != nil {
|
if err := b.Core.remountSecretsEngine(revokeCtx, fromPathDetails, toPathDetails, !b.Core.perfStandby); err != nil {
|
||||||
b.Backend.Logger().Error("remount failed", "from_path", fromPath, "to_path", toPath, "error", err)
|
return err
|
||||||
return handleError(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the view path if available
|
if err := revokeCtx.Err(); err != nil {
|
||||||
var viewPath string
|
return err
|
||||||
if entry != nil {
|
|
||||||
viewPath = entry.ViewPath()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
logger.Info("Removing the source mount from filtered paths on secondaries")
|
||||||
// Remove from filtered mounts and restart evaluation process
|
// Remove from filtered mounts and restart evaluation process
|
||||||
if err := b.Core.removePathFromFilteredPaths(ctx, ns.Path+fromPath, viewPath); err != nil {
|
if err := b.Core.removePathFromFilteredPaths(revokeCtx, fromPathDetails.GetFullPath(), viewPath); err != nil {
|
||||||
b.Backend.Logger().Error("filtered path removal failed", fromPath, "error", err)
|
return err
|
||||||
return handleError(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update quotas with the new path
|
if err := revokeCtx.Err(); err != nil {
|
||||||
if err := b.Core.quotaManager.HandleRemount(ctx, ns.Path, sanitizePath(fromPath), sanitizePath(toPath)); err != nil {
|
return err
|
||||||
b.Core.logger.Error("failed to update quotas after remount", "ns_path", ns.Path, "from_path", fromPath, "to_path", toPath, "error", err)
|
|
||||||
return handleError(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
logger.Info("Updating quotas associated with the source mount")
|
||||||
|
// Update quotas with the new path and namespace
|
||||||
|
if err := b.Core.quotaManager.HandleRemount(revokeCtx, fromPathDetails, toPathDetails); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := b.Core.setMigrationStatus(migrationID, MigrationSuccessStatus); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Info("Completed mount move operations")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleAuthTuneRead is used to get config settings on a auth path
|
// handleAuthTuneRead is used to get config settings on a auth path
|
||||||
|
@ -1249,6 +1316,34 @@ func (b *SystemBackend) handleAuthTuneRead(ctx context.Context, req *logical.Req
|
||||||
return b.handleTuneReadCommon(ctx, "auth/"+path)
|
return b.handleTuneReadCommon(ctx, "auth/"+path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *SystemBackend) handleRemountStatusCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
|
repState := b.Core.ReplicationState()
|
||||||
|
|
||||||
|
migrationID := data.Get("migration_id").(string)
|
||||||
|
if migrationID == "" {
|
||||||
|
return logical.ErrorResponse(
|
||||||
|
"migrationID must be specified"),
|
||||||
|
logical.ErrInvalidRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationInfo := b.Core.readMigrationStatus(migrationID)
|
||||||
|
if migrationInfo == nil {
|
||||||
|
// If the migration info is not found and this is a perf secondary
|
||||||
|
// forward the request to the primary cluster
|
||||||
|
if repState.HasState(consts.ReplicationPerformanceSecondary) {
|
||||||
|
return nil, logical.ErrReadOnly
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
resp := &logical.Response{
|
||||||
|
Data: map[string]interface{}{
|
||||||
|
"migration_id": migrationID,
|
||||||
|
"migration_info": migrationInfo,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
// handleMountTuneRead is used to get config settings on a backend
|
// handleMountTuneRead is used to get config settings on a backend
|
||||||
func (b *SystemBackend) handleMountTuneRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
func (b *SystemBackend) handleMountTuneRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
path := data.Get("path").(string)
|
path := data.Get("path").(string)
|
||||||
|
@ -4519,7 +4614,7 @@ in the plugin catalog.`,
|
||||||
},
|
},
|
||||||
|
|
||||||
"remount": {
|
"remount": {
|
||||||
"Move the mount point of an already-mounted backend.",
|
"Move the mount point of an already-mounted backend, within or across namespaces",
|
||||||
`
|
`
|
||||||
This path responds to the following HTTP methods.
|
This path responds to the following HTTP methods.
|
||||||
|
|
||||||
|
@ -4528,6 +4623,15 @@ This path responds to the following HTTP methods.
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"remount-status": {
|
||||||
|
"Check the status of a mount move operation",
|
||||||
|
`
|
||||||
|
This path responds to the following HTTP methods.
|
||||||
|
GET /sys/remount/status/:migration_id
|
||||||
|
Check the status of a mount move operation for the given migration_id
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
|
||||||
"auth_tune": {
|
"auth_tune": {
|
||||||
"Tune the configuration parameters for an auth path.",
|
"Tune the configuration parameters for an auth path.",
|
||||||
`Read and write the 'default-lease-ttl' and 'max-lease-ttl' values of
|
`Read and write the 'default-lease-ttl' and 'max-lease-ttl' values of
|
||||||
|
|
|
@ -1308,8 +1308,9 @@ func (b *SystemBackend) leasePaths() []*framework.Path {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *SystemBackend) remountPath() *framework.Path {
|
func (b *SystemBackend) remountPaths() []*framework.Path {
|
||||||
return &framework.Path{
|
return []*framework.Path{
|
||||||
|
{
|
||||||
Pattern: "remount",
|
Pattern: "remount",
|
||||||
|
|
||||||
Fields: map[string]*framework.FieldSchema{
|
Fields: map[string]*framework.FieldSchema{
|
||||||
|
@ -1323,12 +1324,34 @@ func (b *SystemBackend) remountPath() *framework.Path {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
Operations: map[logical.Operation]framework.OperationHandler{
|
||||||
logical.UpdateOperation: b.handleRemount,
|
logical.UpdateOperation: &framework.PathOperation{
|
||||||
|
Callback: b.handleRemount,
|
||||||
|
Summary: "Initiate a mount migration",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
HelpSynopsis: strings.TrimSpace(sysHelp["remount"][0]),
|
HelpSynopsis: strings.TrimSpace(sysHelp["remount"][0]),
|
||||||
HelpDescription: strings.TrimSpace(sysHelp["remount"][1]),
|
HelpDescription: strings.TrimSpace(sysHelp["remount"][1]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Pattern: "remount/status/(?P<migration_id>.+?)$",
|
||||||
|
|
||||||
|
Fields: map[string]*framework.FieldSchema{
|
||||||
|
"migration_id": {
|
||||||
|
Type: framework.TypeString,
|
||||||
|
Description: "The ID of the migration operation",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
Operations: map[logical.Operation]framework.OperationHandler{
|
||||||
|
logical.ReadOperation: &framework.PathOperation{
|
||||||
|
Callback: b.handleRemountStatusCheck,
|
||||||
|
Summary: "Check status of a mount migration",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
HelpSynopsis: strings.TrimSpace(sysHelp["remount-status"][0]),
|
||||||
|
HelpDescription: strings.TrimSpace(sysHelp["remount-status"][1]),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -212,10 +212,10 @@ func (b *SystemBackend) handleRateLimitQuotasUpdate() framework.OperationFunc {
|
||||||
case quota == nil:
|
case quota == nil:
|
||||||
quota = quotas.NewRateLimitQuota(name, ns.Path, mountPath, rate, interval, blockInterval)
|
quota = quotas.NewRateLimitQuota(name, ns.Path, mountPath, rate, interval, blockInterval)
|
||||||
default:
|
default:
|
||||||
rlq := quota.(*quotas.RateLimitQuota)
|
|
||||||
// Re-inserting the already indexed object in memdb might cause problems.
|
// Re-inserting the already indexed object in memdb might cause problems.
|
||||||
// So, clone the object. See https://github.com/hashicorp/go-memdb/issues/76.
|
// So, clone the object. See https://github.com/hashicorp/go-memdb/issues/76.
|
||||||
rlq = rlq.Clone()
|
clonedQuota := quota.Clone()
|
||||||
|
rlq := clonedQuota.(*quotas.RateLimitQuota)
|
||||||
rlq.NamespacePath = ns.Path
|
rlq.NamespacePath = ns.Path
|
||||||
rlq.MountPath = mountPath
|
rlq.MountPath = mountPath
|
||||||
rlq.Rate = rate
|
rlq.Rate = rate
|
||||||
|
|
|
@ -691,12 +691,18 @@ func TestSystemBackend_remount(t *testing.T) {
|
||||||
req.Data["to"] = "foo"
|
req.Data["to"] = "foo"
|
||||||
req.Data["config"] = structs.Map(MountConfig{})
|
req.Data["config"] = structs.Map(MountConfig{})
|
||||||
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
|
resp, err := b.HandleRequest(namespace.RootContext(nil), req)
|
||||||
|
RetryUntil(t, 5*time.Second, func() error {
|
||||||
|
req = logical.TestRequest(t, logical.ReadOperation, fmt.Sprintf("remount/status/%s", resp.Data["migration_id"]))
|
||||||
|
resp, err = b.HandleRequest(namespace.RootContext(nil), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
if resp != nil {
|
migrationInfo := resp.Data["migration_info"].(*MountMigrationInfo)
|
||||||
t.Fatalf("bad: %v", resp)
|
if migrationInfo.MigrationStatus != MigrationSuccessStatus.String() {
|
||||||
|
return fmt.Errorf("Expected migration status to be successful, got %q", migrationInfo.MigrationStatus)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSystemBackend_remount_invalid(t *testing.T) {
|
func TestSystemBackend_remount_invalid(t *testing.T) {
|
||||||
|
@ -710,8 +716,8 @@ func TestSystemBackend_remount_invalid(t *testing.T) {
|
||||||
if err != logical.ErrInvalidRequest {
|
if err != logical.ErrInvalidRequest {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
if resp.Data["error"] != `no matching mount at "unknown/"` {
|
if !strings.Contains(resp.Data["error"].(string), "no matching mount at \"unknown/\"") {
|
||||||
t.Fatalf("bad: %v", resp)
|
t.Fatalf("Found unexpected error %q", resp.Data["error"].(string))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -725,8 +731,8 @@ func TestSystemBackend_remount_system(t *testing.T) {
|
||||||
if err != logical.ErrInvalidRequest {
|
if err != logical.ErrInvalidRequest {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
if resp.Data["error"] != `cannot remount "sys/"` {
|
if !strings.Contains(resp.Data["error"].(string), "cannot remount \"sys/\"") {
|
||||||
t.Fatalf("bad: %v", resp)
|
t.Fatalf("Found unexpected error %q", resp.Data["error"].(string))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -741,7 +747,7 @@ func TestSystemBackend_remount_clean(t *testing.T) {
|
||||||
if err != logical.ErrInvalidRequest {
|
if err != logical.ErrInvalidRequest {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
if resp.Data["error"] != `'to' path 'foo//bar' does not match cleaned path 'foo/bar'` {
|
if resp.Data["error"] != `invalid destination mount: path 'foo//bar/' does not match cleaned path 'foo/bar/'` {
|
||||||
t.Fatalf("bad: %v", resp)
|
t.Fatalf("bad: %v", resp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -757,7 +763,7 @@ func TestSystemBackend_remount_nonPrintable(t *testing.T) {
|
||||||
if err != logical.ErrInvalidRequest {
|
if err != logical.ErrInvalidRequest {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
if resp.Data["error"] != `'to' path cannot contain non-printable characters` {
|
if resp.Data["error"] != `invalid destination mount: path cannot contain non-printable characters` {
|
||||||
t.Fatalf("bad: %v", resp)
|
t.Fatalf("bad: %v", resp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
197
vault/mount.go
197
vault/mount.go
|
@ -126,6 +126,32 @@ type MountTable struct {
|
||||||
Entries []*MountEntry `json:"entries"`
|
Entries []*MountEntry `json:"entries"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MountMigrationStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
MigrationInProgressStatus MountMigrationStatus = iota
|
||||||
|
MigrationSuccessStatus
|
||||||
|
MigrationFailureStatus
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m MountMigrationStatus) String() string {
|
||||||
|
switch m {
|
||||||
|
case MigrationInProgressStatus:
|
||||||
|
return "in-progress"
|
||||||
|
case MigrationSuccessStatus:
|
||||||
|
return "success"
|
||||||
|
case MigrationFailureStatus:
|
||||||
|
return "failure"
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
type MountMigrationInfo struct {
|
||||||
|
SourceMount string `json:"source_mount"`
|
||||||
|
TargetMount string `json:"target_mount"`
|
||||||
|
MigrationStatus string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
// tableMetrics is responsible for setting gauge metrics for
|
// tableMetrics is responsible for setting gauge metrics for
|
||||||
// mount table storage sizes (in bytes) and mount table num
|
// mount table storage sizes (in bytes) and mount table num
|
||||||
// entries. It does this via setGaugeWithLabels. It then
|
// entries. It does this via setGaugeWithLabels. It then
|
||||||
|
@ -195,14 +221,10 @@ func (t *MountTable) shallowClone() *MountTable {
|
||||||
|
|
||||||
// setTaint is used to set the taint on given entry Accepts either the mount
|
// setTaint is used to set the taint on given entry Accepts either the mount
|
||||||
// entry's path or namespace + path, i.e. <ns-path>/secret/ or <ns-path>/token/
|
// entry's path or namespace + path, i.e. <ns-path>/secret/ or <ns-path>/token/
|
||||||
func (t *MountTable) setTaint(ctx context.Context, path string, tainted bool, mountState string) (*MountEntry, error) {
|
func (t *MountTable) setTaint(nsID, path string, tainted bool, mountState string) (*MountEntry, error) {
|
||||||
n := len(t.Entries)
|
n := len(t.Entries)
|
||||||
ns, err := namespace.FromContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
if entry := t.Entries[i]; entry.Path == path && entry.Namespace().ID == ns.ID {
|
if entry := t.Entries[i]; entry.Path == path && entry.Namespace().ID == nsID {
|
||||||
t.Entries[i].Tainted = tainted
|
t.Entries[i].Tainted = tainted
|
||||||
t.Entries[i].MountState = mountState
|
t.Entries[i].MountState = mountState
|
||||||
return t.Entries[i], nil
|
return t.Entries[i], nil
|
||||||
|
@ -662,7 +684,7 @@ func (c *Core) unmountInternal(ctx context.Context, path string, updateStorage b
|
||||||
entry := c.router.MatchingMountEntry(ctx, path)
|
entry := c.router.MatchingMountEntry(ctx, path)
|
||||||
|
|
||||||
// Mark the entry as tainted
|
// Mark the entry as tainted
|
||||||
if err := c.taintMountEntry(ctx, path, updateStorage, true); err != nil {
|
if err := c.taintMountEntry(ctx, ns.ID, path, updateStorage, true); err != nil {
|
||||||
c.logger.Error("failed to taint mount entry for path being unmounted", "error", err, "path", path)
|
c.logger.Error("failed to taint mount entry for path being unmounted", "error", err, "path", path)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -780,7 +802,7 @@ func (c *Core) removeMountEntry(ctx context.Context, path string, updateStorage
|
||||||
}
|
}
|
||||||
|
|
||||||
// taintMountEntry is used to mark an entry in the mount table as tainted
|
// taintMountEntry is used to mark an entry in the mount table as tainted
|
||||||
func (c *Core) taintMountEntry(ctx context.Context, path string, updateStorage, unmounting bool) error {
|
func (c *Core) taintMountEntry(ctx context.Context, nsID, mountPath string, updateStorage, unmounting bool) error {
|
||||||
c.mountsLock.Lock()
|
c.mountsLock.Lock()
|
||||||
defer c.mountsLock.Unlock()
|
defer c.mountsLock.Unlock()
|
||||||
|
|
||||||
|
@ -791,12 +813,12 @@ func (c *Core) taintMountEntry(ctx context.Context, path string, updateStorage,
|
||||||
|
|
||||||
// As modifying the taint of an entry affects shallow clones,
|
// As modifying the taint of an entry affects shallow clones,
|
||||||
// we simply use the original
|
// we simply use the original
|
||||||
entry, err := c.mounts.setTaint(ctx, path, true, mountState)
|
entry, err := c.mounts.setTaint(nsID, mountPath, true, mountState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
c.logger.Error("nil entry found tainting entry in mounts table", "path", path)
|
c.logger.Error("nil entry found tainting entry in mounts table", "path", mountPath)
|
||||||
return logical.CodedError(500, "failed to taint entry in mounts table")
|
return logical.CodedError(500, "failed to taint entry in mounts table")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -846,99 +868,90 @@ func (c *Core) remountForceInternal(ctx context.Context, path string, updateStor
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remount is used to remount a path at a new mount point.
|
func (c *Core) remountSecretsEngineCurrentNamespace(ctx context.Context, src, dst string, updateStorage bool) error {
|
||||||
func (c *Core) remount(ctx context.Context, src, dst string, updateStorage bool) error {
|
|
||||||
ns, err := namespace.FromContext(ctx)
|
ns, err := namespace.FromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we end the path in a slash
|
srcPathDetails := c.splitNamespaceAndMountFromPath(ns.Path, src)
|
||||||
if !strings.HasSuffix(src, "/") {
|
dstPathDetails := c.splitNamespaceAndMountFromPath(ns.Path, dst)
|
||||||
src += "/"
|
return c.remountSecretsEngine(ctx, srcPathDetails, dstPathDetails, updateStorage)
|
||||||
}
|
}
|
||||||
if !strings.HasSuffix(dst, "/") {
|
|
||||||
dst += "/"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent protected paths from being remounted
|
// remountSecretsEngine is used to remount a path at a new mount point.
|
||||||
for _, p := range protectedMounts {
|
func (c *Core) remountSecretsEngine(ctx context.Context, src, dst namespace.MountPathDetails, updateStorage bool) error {
|
||||||
if strings.HasPrefix(src, p) {
|
ns, err := namespace.FromContext(ctx)
|
||||||
return fmt.Errorf("cannot remount %q", src)
|
if err != nil {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify exact match of the route
|
|
||||||
srcMatch := c.router.MatchingMountEntry(ctx, src)
|
|
||||||
if srcMatch == nil {
|
|
||||||
return fmt.Errorf("no matching mount at %q", src)
|
|
||||||
}
|
|
||||||
if srcMatch.NamespaceID != ns.ID {
|
|
||||||
return fmt.Errorf("source mount in a different namespace than request")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := verifyNamespace(c, ns, &MountEntry{Path: dst}); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if match := c.router.MatchingMount(ctx, dst); match != "" {
|
// Prevent protected paths from being remounted, or target mounts being in protected paths
|
||||||
|
for _, p := range protectedMounts {
|
||||||
|
if strings.HasPrefix(src.MountPath, p) {
|
||||||
|
return fmt.Errorf("cannot remount %q", src.MountPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(dst.MountPath, p) {
|
||||||
|
return fmt.Errorf("cannot remount to destination %+v", dst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
srcRelativePath := src.GetRelativePath(ns)
|
||||||
|
dstRelativePath := dst.GetRelativePath(ns)
|
||||||
|
|
||||||
|
// Verify exact match of the route
|
||||||
|
srcMatch := c.router.MatchingMountEntry(ctx, srcRelativePath)
|
||||||
|
if srcMatch == nil {
|
||||||
|
return fmt.Errorf("no matching mount at %+v", src)
|
||||||
|
}
|
||||||
|
|
||||||
|
if match := c.router.MatchingMount(ctx, dstRelativePath); match != "" {
|
||||||
return fmt.Errorf("existing mount at %q", match)
|
return fmt.Errorf("existing mount at %q", match)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark the entry as tainted
|
// Mark the entry as tainted
|
||||||
if err := c.taintMountEntry(ctx, src, updateStorage, false); err != nil {
|
if err := c.taintMountEntry(ctx, src.Namespace.ID, src.MountPath, updateStorage, false); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Taint the router path to prevent routing
|
// Taint the router path to prevent routing
|
||||||
if err := c.router.Taint(ctx, src); err != nil {
|
if err := c.router.Taint(ctx, srcRelativePath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !c.IsDRSecondary() {
|
if !c.IsDRSecondary() {
|
||||||
// Invoke the rollback manager a final time
|
// Invoke the rollback manager a final time
|
||||||
rCtx := namespace.ContextWithNamespace(c.activeContext, ns)
|
rCtx := namespace.ContextWithNamespace(c.activeContext, ns)
|
||||||
if c.rollback != nil {
|
if c.rollback != nil && c.router.MatchingBackend(ctx, srcRelativePath) != nil {
|
||||||
if err := c.rollback.Rollback(rCtx, src); err != nil {
|
if err := c.rollback.Rollback(rCtx, srcRelativePath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if entry := c.router.MatchingMountEntry(ctx, src); entry == nil {
|
revokeCtx := namespace.ContextWithNamespace(ctx, src.Namespace)
|
||||||
return fmt.Errorf("no matching mount at %q", src)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Revoke all the dynamic keys
|
// Revoke all the dynamic keys
|
||||||
if err := c.expiration.RevokePrefix(rCtx, src, true); err != nil {
|
if err := c.expiration.RevokePrefix(revokeCtx, src.MountPath, true); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.mountsLock.Lock()
|
c.mountsLock.Lock()
|
||||||
if match := c.router.MatchingMount(ctx, dst); match != "" {
|
if match := c.router.MatchingMount(ctx, dstRelativePath); match != "" {
|
||||||
c.mountsLock.Unlock()
|
c.mountsLock.Unlock()
|
||||||
return fmt.Errorf("existing mount at %q", match)
|
return fmt.Errorf("existing mount at %q", match)
|
||||||
}
|
}
|
||||||
var entry *MountEntry
|
|
||||||
for _, mountEntry := range c.mounts.Entries {
|
|
||||||
if mountEntry.Path == src && mountEntry.NamespaceID == ns.ID {
|
|
||||||
entry = mountEntry
|
|
||||||
entry.Path = dst
|
|
||||||
entry.Tainted = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if entry == nil {
|
srcMatch.Tainted = false
|
||||||
c.mountsLock.Unlock()
|
srcMatch.NamespaceID = dst.Namespace.ID
|
||||||
c.logger.Error("failed to find entry in mounts table")
|
srcMatch.namespace = dst.Namespace
|
||||||
return logical.CodedError(500, "failed to find entry in mounts table")
|
srcPath := srcMatch.Path
|
||||||
}
|
srcMatch.Path = dst.MountPath
|
||||||
|
|
||||||
// Update the mount table
|
// Update the mount table
|
||||||
if err := c.persistMounts(ctx, c.mounts, &entry.Local); err != nil {
|
if err := c.persistMounts(ctx, c.mounts, &srcMatch.Local); err != nil {
|
||||||
entry.Path = src
|
srcMatch.Path = srcPath
|
||||||
entry.Tainted = true
|
srcMatch.Tainted = true
|
||||||
c.mountsLock.Unlock()
|
c.mountsLock.Unlock()
|
||||||
if err == logical.ErrReadOnly && c.perfStandby {
|
if err == logical.ErrReadOnly && c.perfStandby {
|
||||||
return err
|
return err
|
||||||
|
@ -949,23 +962,37 @@ func (c *Core) remount(ctx context.Context, src, dst string, updateStorage bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remount the backend
|
// Remount the backend
|
||||||
if err := c.router.Remount(ctx, src, dst); err != nil {
|
if err := c.router.Remount(ctx, srcRelativePath, dstRelativePath); err != nil {
|
||||||
c.mountsLock.Unlock()
|
c.mountsLock.Unlock()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
c.mountsLock.Unlock()
|
c.mountsLock.Unlock()
|
||||||
|
|
||||||
// Un-taint the path
|
// Un-taint the path
|
||||||
if err := c.router.Untaint(ctx, dst); err != nil {
|
if err := c.router.Untaint(ctx, dstRelativePath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.logger.IsInfo() {
|
|
||||||
c.logger.Info("successful remount", "old_path", src, "new_path", dst)
|
c.logger.Info("successful remount", "old_path", src, "new_path", dst)
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From an input path that has a relative namespace heirarchy followed by a mount point, return the full
|
||||||
|
// namespace of the mount point, along with the mount point without the namespace related prefix.
|
||||||
|
// For example, in a heirarchy ns1/ns2/ns3/secret-mount, when currNs is ns1 and path is ns2/ns3/secret-mount,
|
||||||
|
// this returns the namespace object for ns1/ns2/ns3/, and the string "secret-mount"
|
||||||
|
func (c *Core) splitNamespaceAndMountFromPath(currNs, path string) namespace.MountPathDetails {
|
||||||
|
fullPath := currNs + path
|
||||||
|
fullNs := c.namespaceByPath(fullPath)
|
||||||
|
|
||||||
|
mountPath := strings.TrimPrefix(fullPath, fullNs.Path)
|
||||||
|
|
||||||
|
return namespace.MountPathDetails{
|
||||||
|
Namespace: fullNs,
|
||||||
|
MountPath: sanitizePath(mountPath),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// loadMounts is invoked as part of postUnseal to load the mount table
|
// loadMounts is invoked as part of postUnseal to load the mount table
|
||||||
func (c *Core) loadMounts(ctx context.Context) error {
|
func (c *Core) loadMounts(ctx context.Context) error {
|
||||||
// Load the existing mount table
|
// Load the existing mount table
|
||||||
|
@ -1580,3 +1607,37 @@ func (c *Core) setCoreBackend(entry *MountEntry, backend logical.Backend, view *
|
||||||
c.identityStore = backend.(*IdentityStore)
|
c.identityStore = backend.(*IdentityStore)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Core) createMigrationStatus(from, to namespace.MountPathDetails) (string, error) {
|
||||||
|
migrationID, err := uuid.GenerateUUID()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error generating uuid for mount move invocation: %w", err)
|
||||||
|
}
|
||||||
|
migrationInfo := MountMigrationInfo{
|
||||||
|
SourceMount: from.Namespace.Path + from.MountPath,
|
||||||
|
TargetMount: to.Namespace.Path + to.MountPath,
|
||||||
|
MigrationStatus: MigrationInProgressStatus.String(),
|
||||||
|
}
|
||||||
|
c.mountMigrationTracker.Store(migrationID, migrationInfo)
|
||||||
|
return migrationID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Core) setMigrationStatus(migrationID string, migrationStatus MountMigrationStatus) error {
|
||||||
|
migrationInfoRaw, ok := c.mountMigrationTracker.Load(migrationID)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("Migration Tracker entry missing for ID %s", migrationID)
|
||||||
|
}
|
||||||
|
migrationInfo := migrationInfoRaw.(MountMigrationInfo)
|
||||||
|
migrationInfo.MigrationStatus = migrationStatus.String()
|
||||||
|
c.mountMigrationTracker.Store(migrationID, migrationInfo)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Core) readMigrationStatus(migrationID string) *MountMigrationInfo {
|
||||||
|
migrationInfoRaw, ok := c.mountMigrationTracker.Load(migrationID)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
migrationInfo := migrationInfoRaw.(MountMigrationInfo)
|
||||||
|
return &migrationInfo
|
||||||
|
}
|
||||||
|
|
|
@ -476,7 +476,7 @@ func TestCore_RemountConcurrent(t *testing.T) {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
err := c2.remount(namespace.RootContext(nil), "test1", "foo", true)
|
err := c2.remountSecretsEngineCurrentNamespace(namespace.RootContext(nil), "test1", "foo", true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logf("err: %v", err)
|
t.Logf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -485,7 +485,7 @@ func TestCore_RemountConcurrent(t *testing.T) {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
err := c2.remount(namespace.RootContext(nil), "test2", "foo", true)
|
err := c2.remountSecretsEngineCurrentNamespace(namespace.RootContext(nil), "test2", "foo", true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Logf("err: %v", err)
|
t.Logf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -504,7 +504,7 @@ func TestCore_RemountConcurrent(t *testing.T) {
|
||||||
|
|
||||||
func TestCore_Remount(t *testing.T) {
|
func TestCore_Remount(t *testing.T) {
|
||||||
c, keys, _ := TestCoreUnsealed(t)
|
c, keys, _ := TestCoreUnsealed(t)
|
||||||
err := c.remount(namespace.RootContext(nil), "secret", "foo", true)
|
err := c.remountSecretsEngineCurrentNamespace(namespace.RootContext(nil), "secret", "foo", true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -612,7 +612,7 @@ func TestCore_Remount_Cleanup(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remount, this should cleanup
|
// Remount, this should cleanup
|
||||||
if err := c.remount(namespace.RootContext(nil), "test/", "new/", true); err != nil {
|
if err := c.remountSecretsEngineCurrentNamespace(namespace.RootContext(nil), "test/", "new/", true); err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -641,7 +641,7 @@ func TestCore_Remount_Cleanup(t *testing.T) {
|
||||||
|
|
||||||
func TestCore_Remount_Protected(t *testing.T) {
|
func TestCore_Remount_Protected(t *testing.T) {
|
||||||
c, _, _ := TestCoreUnsealed(t)
|
c, _, _ := TestCoreUnsealed(t)
|
||||||
err := c.remount(namespace.RootContext(nil), "sys", "foo", true)
|
err := c.remountSecretsEngineCurrentNamespace(namespace.RootContext(nil), "sys", "foo", true)
|
||||||
if err.Error() != `cannot remount "sys/"` {
|
if err.Error() != `cannot remount "sys/"` {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
log "github.com/hashicorp/go-hclog"
|
log "github.com/hashicorp/go-hclog"
|
||||||
"github.com/hashicorp/go-memdb"
|
"github.com/hashicorp/go-memdb"
|
||||||
"github.com/hashicorp/vault/helper/metricsutil"
|
"github.com/hashicorp/vault/helper/metricsutil"
|
||||||
|
"github.com/hashicorp/vault/helper/namespace"
|
||||||
"github.com/hashicorp/vault/sdk/helper/pathmanager"
|
"github.com/hashicorp/vault/sdk/helper/pathmanager"
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
)
|
)
|
||||||
|
@ -183,8 +184,11 @@ type Quota interface {
|
||||||
// rule is deleted.
|
// rule is deleted.
|
||||||
close(context.Context) error
|
close(context.Context) error
|
||||||
|
|
||||||
// handleRemount takes in the new mount path in the quota
|
// Clone creates a clone of the calling quota
|
||||||
handleRemount(string)
|
Clone() Quota
|
||||||
|
|
||||||
|
// handleRemount updates the mount and namesapce paths of the quota
|
||||||
|
handleRemount(string, string)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Response holds information about the result of the Allow() call. The response
|
// Response holds information about the result of the Allow() call. The response
|
||||||
|
@ -268,17 +272,41 @@ func (m *Manager) SetQuota(ctx context.Context, qType string, quota Quota, loadi
|
||||||
return m.setQuotaLocked(ctx, qType, quota, loading)
|
return m.setQuotaLocked(ctx, qType, quota, loading)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setQuotaLocked adds or updates a quota rule, modifying the db as well as
|
// setQuotaLocked creates a transaction, passes it into setQuotaLockedWithTxn and manages its lifecycle
|
||||||
// any runtime elements such as goroutines.
|
// along with updating lease quota counts
|
||||||
// It should be called with the write lock held.
|
|
||||||
func (m *Manager) setQuotaLocked(ctx context.Context, qType string, quota Quota, loading bool) error {
|
func (m *Manager) setQuotaLocked(ctx context.Context, qType string, quota Quota, loading bool) error {
|
||||||
|
txn := m.db.Txn(true)
|
||||||
|
defer txn.Abort()
|
||||||
|
|
||||||
|
err := m.setQuotaLockedWithTxn(ctx, qType, quota, loading, txn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if loading {
|
||||||
|
txn.Commit()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// For the lease count type, recompute the counters
|
||||||
|
if !loading && qType == TypeLeaseCount.String() {
|
||||||
|
if err := m.recomputeLeaseCounts(ctx, txn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
txn.Commit()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// setQuotaLockedWithTxn adds or updates a quota rule, modifying the db as well as
|
||||||
|
// any runtime elements such as goroutines, using the transaction passed in
|
||||||
|
// It should be called with the write lock held.
|
||||||
|
func (m *Manager) setQuotaLockedWithTxn(ctx context.Context, qType string, quota Quota, loading bool, txn *memdb.Txn) error {
|
||||||
if qType == TypeLeaseCount.String() {
|
if qType == TypeLeaseCount.String() {
|
||||||
m.setIsPerfStandby(quota)
|
m.setIsPerfStandby(quota)
|
||||||
}
|
}
|
||||||
|
|
||||||
txn := m.db.Txn(true)
|
|
||||||
defer txn.Abort()
|
|
||||||
|
|
||||||
raw, err := txn.First(qType, indexID, quota.quotaID())
|
raw, err := txn.First(qType, indexID, quota.quotaID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -306,19 +334,6 @@ func (m *Manager) setQuotaLocked(ctx context.Context, qType string, quota Quota,
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if loading {
|
|
||||||
txn.Commit()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// For the lease count type, recompute the counters
|
|
||||||
if !loading && qType == TypeLeaseCount.String() {
|
|
||||||
if err := m.recomputeLeaseCounts(ctx, txn); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
txn.Commit()
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -937,23 +952,30 @@ func QuotaStoragePath(quotaType, name string) string {
|
||||||
|
|
||||||
// HandleRemount updates the quota subsystem about the remount operation that
|
// HandleRemount updates the quota subsystem about the remount operation that
|
||||||
// took place. Quota manager will trigger the quota specific updates including
|
// took place. Quota manager will trigger the quota specific updates including
|
||||||
// the mount path update..
|
// the mount path update and the namespace update
|
||||||
func (m *Manager) HandleRemount(ctx context.Context, nsPath, fromPath, toPath string) error {
|
func (m *Manager) HandleRemount(ctx context.Context, from, to namespace.MountPathDetails) error {
|
||||||
m.lock.Lock()
|
m.lock.Lock()
|
||||||
defer m.lock.Unlock()
|
defer m.lock.Unlock()
|
||||||
|
|
||||||
|
// Grab a write transaction, as we want to save the updated quota in memdb
|
||||||
txn := m.db.Txn(true)
|
txn := m.db.Txn(true)
|
||||||
defer txn.Abort()
|
defer txn.Abort()
|
||||||
|
|
||||||
// nsPath would have been made non-empty during insertion. Use non-empty value
|
// quota namespace would have been made non-empty during insertion. Use non-empty value
|
||||||
// during query as well.
|
// during query as well.
|
||||||
if nsPath == "" {
|
fromNs := from.Namespace.Path
|
||||||
nsPath = "root"
|
if fromNs == "" {
|
||||||
|
fromNs = namespace.RootNamespaceID
|
||||||
|
}
|
||||||
|
|
||||||
|
toNs := to.Namespace.Path
|
||||||
|
if toNs == "" {
|
||||||
|
toNs = namespace.RootNamespaceID
|
||||||
}
|
}
|
||||||
|
|
||||||
idx := indexNamespaceMount
|
idx := indexNamespaceMount
|
||||||
leaseQuotaUpdated := false
|
leaseQuotaUpdated := false
|
||||||
args := []interface{}{nsPath, fromPath}
|
args := []interface{}{fromNs, from.MountPath}
|
||||||
for _, quotaType := range quotaTypes() {
|
for _, quotaType := range quotaTypes() {
|
||||||
iter, err := txn.Get(quotaType, idx, args...)
|
iter, err := txn.Get(quotaType, idx, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -961,7 +983,11 @@ func (m *Manager) HandleRemount(ctx context.Context, nsPath, fromPath, toPath st
|
||||||
}
|
}
|
||||||
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
||||||
quota := raw.(Quota)
|
quota := raw.(Quota)
|
||||||
quota.handleRemount(toPath)
|
|
||||||
|
// Clone the object and update it
|
||||||
|
clonedQuota := quota.Clone()
|
||||||
|
clonedQuota.handleRemount(to.MountPath, toNs)
|
||||||
|
// Update both underlying storage and memdb with the quota change
|
||||||
entry, err := logical.StorageEntryJSON(QuotaStoragePath(quotaType, quota.QuotaName()), quota)
|
entry, err := logical.StorageEntryJSON(QuotaStoragePath(quotaType, quota.QuotaName()), quota)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -969,6 +995,9 @@ func (m *Manager) HandleRemount(ctx context.Context, nsPath, fromPath, toPath st
|
||||||
if err := m.storage.Put(ctx, entry); err != nil {
|
if err := m.storage.Put(ctx, entry); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := m.setQuotaLockedWithTxn(ctx, quotaType, clonedQuota, false, txn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if quotaType == TypeLeaseCount.String() {
|
if quotaType == TypeLeaseCount.String() {
|
||||||
leaseQuotaUpdated = true
|
leaseQuotaUpdated = true
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,7 +101,7 @@ func NewRateLimitQuota(name, nsPath, mountPath string, rate float64, interval, b
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *RateLimitQuota) Clone() *RateLimitQuota {
|
func (q *RateLimitQuota) Clone() Quota {
|
||||||
rlq := &RateLimitQuota{
|
rlq := &RateLimitQuota{
|
||||||
ID: q.ID,
|
ID: q.ID,
|
||||||
Name: q.Name,
|
Name: q.Name,
|
||||||
|
@ -337,6 +337,7 @@ func (rlq *RateLimitQuota) close(ctx context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rlq *RateLimitQuota) handleRemount(toPath string) {
|
func (rlq *RateLimitQuota) handleRemount(mountpath, nspath string) {
|
||||||
rlq.MountPath = toPath
|
rlq.MountPath = mountpath
|
||||||
|
rlq.NamespacePath = nspath
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ func TestQuotas_MountPathOverwrite(t *testing.T) {
|
||||||
|
|
||||||
quota := NewRateLimitQuota("tq", "", "kv1/", 10, time.Second, 0)
|
quota := NewRateLimitQuota("tq", "", "kv1/", 10, time.Second, 0)
|
||||||
require.NoError(t, qm.SetQuota(context.Background(), TypeRateLimit.String(), quota, false))
|
require.NoError(t, qm.SetQuota(context.Background(), TypeRateLimit.String(), quota, false))
|
||||||
quota = quota.Clone()
|
quota = quota.Clone().(*RateLimitQuota)
|
||||||
quota.MountPath = "kv2/"
|
quota.MountPath = "kv2/"
|
||||||
require.NoError(t, qm.SetQuota(context.Background(), TypeRateLimit.String(), quota, false))
|
require.NoError(t, qm.SetQuota(context.Background(), TypeRateLimit.String(), quota, false))
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,10 @@ func (l LeaseCountQuota) close(_ context.Context) error {
|
||||||
panic("implement me")
|
panic("implement me")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l LeaseCountQuota) handleRemount(s string) {
|
func (l LeaseCountQuota) Clone() Quota {
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LeaseCountQuota) handleRemount(mountPath, nsPath string) {
|
||||||
panic("implement me")
|
panic("implement me")
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,7 @@ values set here cannot be changed after key creation.
|
||||||
- `rsa-3072` - RSA with bit size of 3072 (asymmetric)
|
- `rsa-3072` - RSA with bit size of 3072 (asymmetric)
|
||||||
- `rsa-4096` - RSA with bit size of 4096 (asymmetric)
|
- `rsa-4096` - RSA with bit size of 4096 (asymmetric)
|
||||||
|
|
||||||
- `auto_rotate_interval` `(duration: "0", optional)` – The interval at which
|
- `auto_rotate_period` `(duration: "0", optional)` – The period at which
|
||||||
this key should be rotated automatically. Setting this to "0" (the default)
|
this key should be rotated automatically. Setting this to "0" (the default)
|
||||||
will disable automatic key rotation. This value cannot be shorter than one
|
will disable automatic key rotation. This value cannot be shorter than one
|
||||||
hour.
|
hour.
|
||||||
|
@ -232,10 +232,10 @@ are returned during a read operation on the named key.)
|
||||||
- `allow_plaintext_backup` `(bool: false)` - If set, enables taking backup of
|
- `allow_plaintext_backup` `(bool: false)` - If set, enables taking backup of
|
||||||
named key in the plaintext format. Once set, this cannot be disabled.
|
named key in the plaintext format. Once set, this cannot be disabled.
|
||||||
|
|
||||||
- `auto_rotate_interval` `(duration: "", optional)` – The interval at which this
|
- `auto_rotate_period` `(duration: "", optional)` – The period at which this
|
||||||
key should be rotated automatically. Setting this to "0" will disable automatic
|
key should be rotated automatically. Setting this to "0" will disable automatic
|
||||||
key rotation. This value cannot be shorter than one hour. When no value is
|
key rotation. This value cannot be shorter than one hour. When no value is
|
||||||
provided, the interval remains unchanged.
|
provided, the period remains unchanged.
|
||||||
|
|
||||||
### Sample Payload
|
### Sample Payload
|
||||||
|
|
||||||
|
|
|
@ -128,6 +128,7 @@ Plugin authors who wish to have their plugins listed may file a submission via a
|
||||||
|
|
||||||
- [Jenkins](https://plugins.jenkins.io/hashicorp-vault-plugin)
|
- [Jenkins](https://plugins.jenkins.io/hashicorp-vault-plugin)
|
||||||
- [Terraform Enterprise/Terraform Cloud](https://github.com/gitrgoliveira/vault-plugin-auth-tfe)
|
- [Terraform Enterprise/Terraform Cloud](https://github.com/gitrgoliveira/vault-plugin-auth-tfe)
|
||||||
|
- [SSH](https://github.com/42wim/vault-plugin-auth-ssh)
|
||||||
|
|
||||||
### Secrets
|
### Secrets
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue