diff --git a/sdk/queue/priority_queue.go b/sdk/queue/priority_queue.go index 802a538587..1864cfa05d 100644 --- a/sdk/queue/priority_queue.go +++ b/sdk/queue/priority_queue.go @@ -153,6 +153,19 @@ func (pq *PriorityQueue) PopByKey(key string) (*Item, error) { return nil, nil } +// PeekByKey returns the item with the given key without removing it from the queue. +func (pq *PriorityQueue) PeekByKey(id string) *Item { + pq.lock.RLock() + defer pq.lock.RUnlock() + + item, ok := pq.dataMap[id] + if !ok { + return nil + } + + return item +} + // Len returns the number of items in the queue data structure. Do not use this // method directly on the queue, use PriorityQueue.Len() instead. func (q queue) Len() int { return len(q) } diff --git a/sdk/queue/priority_queue_test.go b/sdk/queue/priority_queue_test.go index 108a26cc0e..97612f51aa 100644 --- a/sdk/queue/priority_queue_test.go +++ b/sdk/queue/priority_queue_test.go @@ -141,6 +141,67 @@ func TestPriorityQueue_Pop(t *testing.T) { } } +// TestPriorityQueue_PeekByKey tests the PeekByKey method of PriorityQueue. +// It verifies that PeekByKey returns the correct item without removing it from the queue, +// handles non-existing keys appropriately, works correctly on empty queues, and +// properly handles empty key strings. +func TestPriorityQueue_PeekByKey(t *testing.T) { + pq := New() + tc := testCases() + expectedLength := len(tc) + + // Peek from empty queue + peekedItem := pq.PeekByKey("item-2") + if peekedItem != nil { + t.Fatal("expected nil when peeking from empty queue, got item") + } + if pq.Len() != 0 { + t.Fatalf("expected empty queue to remain size 0, got %d", pq.Len()) + } + + // Push test items + for _, item := range tc { + if err := pq.Push(item); err != nil { + t.Fatal(err) + } + } + + // Peek with empty key + peekedItem = pq.PeekByKey("") + if peekedItem != nil { + t.Fatal("expected nil for empty key, got item") + } + // Verify queue size unchanged + if pq.Len() != expectedLength { + t.Fatalf("expected queue size to remain %d, got %d", expectedLength, pq.Len()) + } + + // Peek at non-existing item + peekedItem = pq.PeekByKey("non-existing-key") + if peekedItem != nil { + t.Fatal("expected nil for non-existing key, got item") + } + // Verify queue size unchanged + if pq.Len() != expectedLength { + t.Fatalf("expected queue size to remain %d, got %d", expectedLength, pq.Len()) + } + + // Peek at a specific item + peekedItem = pq.PeekByKey("item-2") + if peekedItem == nil { + t.Fatal("expected to peek item-2, got nil") + } + // Verify queue size unchanged + if pq.Len() != expectedLength { + t.Fatalf("expected queue size to remain %d, got %d", expectedLength, pq.Len()) + } + // Verify item still exists in queue + stillExists := pq.PeekByKey("item-2") + if stillExists == nil { + t.Fatal("item should still exist after peek") + } +} + func TestPriorityQueue_PopByKey(t *testing.T) { pq := New() diff --git a/vault/core.go b/vault/core.go index 5917607730..10a874a1df 100644 --- a/vault/core.go +++ b/vault/core.go @@ -3993,14 +3993,34 @@ func (c *Core) loadLoginMFAConfigs(ctx context.Context) error { return nil } +// MFACachedAuthResponse represents an authentication response that has been +// temporarily cached during a two-phase MFA (Multi-Factor Authentication) login flow. +// +// This struct is used when an MFA enforcement is configured and a login request +// lacks MFA credentials. Instead of completing the authentication immediately, +// Vault caches the auth response and returns an MFARequirement to the client. +// The client must then complete MFA validation using the mfa/validate endpoint +// to retrieve the cached authentication and receive their token. +// +// The cached response includes the original authentication details along with +// request metadata needed for MFA validation, such as the client's IP address +// for methods like Duo that require connection information. +// +// This struct is also used to cache self-enrollment TOTP MFA secrets generated +// during login when self-enrollment is enabled. This allows Vault to avoid +// persisting the newly generated MFA secret until it has been successfully used +// for validating an MFA-enforced login request. type MFACachedAuthResponse struct { - CachedAuth *logical.Auth - RequestPath string - RequestNSID string - RequestNSPath string - RequestConnRemoteAddr string - TimeOfStorage time.Time - RequestID string + CachedAuth *logical.Auth + RequestPath string + RequestNSID string + RequestNSPath string + RequestConnRemoteAddr string + TimeOfStorage time.Time + RequestID string + SelfEnrollmentMFASecret *mfa.Secret + // Store the secret key string separately to avoid anyone accidentally persisting it on an Entity. + SelfEnrollmentMFASecretKey string } func (c *Core) setupCachedMFAResponseAuth() { diff --git a/vault/identity_store.go b/vault/identity_store.go index 5d909fb0f6..f760fabef7 100644 --- a/vault/identity_store.go +++ b/vault/identity_store.go @@ -165,6 +165,7 @@ func (i *IdentityStore) paths() []*framework.Path { mfaDuoPaths(i), mfaPingIDPaths(i), mfaLoginEnforcementPaths(i), + mfaLoginEnterprisePaths(i), ) } diff --git a/vault/mfa_auth_resp_priority_queue.go b/vault/mfa_auth_resp_priority_queue.go index a2119dffca..7c178c1021 100644 --- a/vault/mfa_auth_resp_priority_queue.go +++ b/vault/mfa_auth_resp_priority_queue.go @@ -4,6 +4,7 @@ package vault import ( + "errors" "sync" "time" @@ -71,6 +72,20 @@ func (pq *LoginMFAPriorityQueue) PopByKey(reqID string) (*MFACachedAuthResponse, return item.Value.(*MFACachedAuthResponse), nil } +// PeekByKey returns the item with the given key without removing it from the queue. +func (pq *LoginMFAPriorityQueue) PeekByKey(reqID string) (*MFACachedAuthResponse, error) { + pq.l.RLock() + defer pq.l.RUnlock() + + item := pq.wrapped.PeekByKey(reqID) + if item == nil { + return nil, errors.New("no item found with the given request ID") + } + + mfaResp := item.Value.(*MFACachedAuthResponse) + return mfaResp, nil +} + // RemoveExpiredMfaAuthResponse pops elements of the queue and check // if the entry has expired or not. If the entry has not expired, it pushes // back the entry to the queue. It returns false if there is no expired element diff --git a/vault/mfa_auth_resp_priority_queue_test.go b/vault/mfa_auth_resp_priority_queue_test.go index 61274aeccd..2931de9799 100644 --- a/vault/mfa_auth_resp_priority_queue_test.go +++ b/vault/mfa_auth_resp_priority_queue_test.go @@ -87,6 +87,89 @@ func TestLoginMFAPriorityQueue_PushPopByKey(t *testing.T) { } } +// TestLoginMFAPriorityQueue_PeekByKey tests the PeekByKey method of +// LoginMFAPriorityQueue. It verifies that PeekByKey returns the correct item +// without removing it from the queue, returns errors on unhappy paths, handles +// non-existing keys appropriately, works correctly on empty queues, and +// properly handles empty key strings. +func TestLoginMFAPriorityQueue_PeekByKey(t *testing.T) { + pq := NewLoginMFAPriorityQueue() + tc := testCases() + expectedLength := len(tc) + + // Peek from empty queue + peekedItem, err := pq.PeekByKey("item-2") + if peekedItem != nil { + t.Fatal("expected nil when peeking from empty queue, got item") + } + if err == nil { + t.Fatal("expected an error when peeking from empty queue, got nil") + } + if pq.Len() != 0 { + t.Fatalf("expected empty queue to remain size 0, got %d", pq.Len()) + } + + // Push test items + for _, item := range tc { + if err := pq.Push(item); err != nil { + t.Fatal(err) + } + } + + // Peek with empty key + peekedItem, err = pq.PeekByKey("") + if peekedItem != nil { + t.Fatal("expected nil for empty key, got item") + } + if err == nil { + t.Fatal("expected error when peeking with empty key , got nil") + } + // Verify queue size unchanged + if pq.Len() != expectedLength { + t.Fatalf("expected queue size to remain %d, got %d", expectedLength, pq.Len()) + } + + // Peek at non-existing item + peekedItem, err = pq.PeekByKey("non-existing-key") + if peekedItem != nil { + t.Fatal("expected nil for non-existing key, got item") + } + if err == nil { + t.Fatal("expected error when peeking with non-existing key, got nil") + } + // Verify queue size unchanged + if pq.Len() != expectedLength { + t.Fatalf("expected queue size to remain %d, got %d", expectedLength, pq.Len()) + } + + // Peek at a specific item + peekedItem, err = pq.PeekByKey(tc[2].RequestID) + if peekedItem == nil { + t.Fatal("expected to peek item-2, got nil") + } + if err != nil { + t.Fatal("expected no error when peeking existing key, got", err) + } + if peekedItem.RequestID != tc[2].RequestID { + t.Fatal("expected the same item on subsequent peeks, got different items") + } + // Verify queue size unchanged + if pq.Len() != expectedLength { + t.Fatalf("expected queue size to remain %d, got %d", expectedLength, pq.Len()) + } + // Verify item still exists in queue + stillExists, err := pq.PeekByKey(tc[2].RequestID) + if stillExists == nil { + t.Fatal("item should still exist after peek") + } + if err != nil { + t.Fatal("expected no error when peeking existing key for the second time, got", err) + } + if stillExists.RequestID != tc[2].RequestID { + t.Fatal("expected the same item on subsequent peeks, got different items") + } +} + func TestLoginMFARemoveStaleEntries(t *testing.T) { pq := NewLoginMFAPriorityQueue()