| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent aa8bb70 commit 888ddb3
4 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -86,6 +86,7 @@ type Container struct { | |||
| 86 | 86 | eventDispatcher *services.EventDispatcher | |
| 87 | 87 | logger telemetry.Logger | |
| 88 | 88 | attachmentRepository repositories.AttachmentRepository | |
| 89 | + userRistrettoCache *ristretto.Cache[string, entities.AuthContext] | ||
| 89 | 90 | } | |
| 90 | 91 | ||
| 91 | 92 | // NewLiteContainer creates a Container without any routes or listeners | |
@@ -1730,8 +1731,11 @@ func (container *Container) PhoneRistrettoCache() (cache *ristretto.Cache[string | |||
| 1730 | 1731 | } | |
| 1731 | 1732 | ||
| 1732 | 1733 | // UserRistrettoCache creates an in-memory *ristretto.Cache[string, entities.AuthContext] | |
| 1733 | - func (container *Container) UserRistrettoCache() (cache *ristretto.Cache[string, entities.AuthContext]) { | ||
| 1734 | - container.logger.Debug(fmt.Sprintf("creating %T", cache)) | ||
| 1734 | + func (container *Container) UserRistrettoCache() *ristretto.Cache[string, entities.AuthContext] { | ||
| 1735 | + if container.userRistrettoCache != nil { | ||
| 1736 | + return container.userRistrettoCache | ||
| 1737 | + } | ||
| 1738 | + container.logger.Debug(fmt.Sprintf("creating %T", container.userRistrettoCache)) | ||
| 1735 | 1739 | ristrettoCache, err := ristretto.NewCache[string, entities.AuthContext](&ristretto.Config[string, entities.AuthContext]{ | |
| 1736 | 1740 | MaxCost: 5000, | |
| 1737 | 1741 | NumCounters: 5000 * 10, | |
@@ -1740,6 +1744,7 @@ func (container *Container) UserRistrettoCache() (cache *ristretto.Cache[string, | |||
| 1740 | 1744 | if err != nil { | |
| 1741 | 1745 | container.logger.Fatal(stacktrace.Propagate(err, "cannot create user ristretto cache")) | |
| 1742 | 1746 | } | |
| 1747 | + container.userRistrettoCache = ristrettoCache | ||
| 1743 | 1748 | return ristrettoCache | |
| 1744 | 1749 | } | |
| 1745 | 1750 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -65,8 +65,13 @@ func (repository *gormUserRepository) RotateAPIKey(ctx context.Context, userID e | |||
| 65 | 65 | } | |
| 66 | 66 | ||
| 67 | 67 | user := new(entities.User) | |
| 68 | + var oldAPIKey string | ||
| 68 | 69 | err = crdbgorm.ExecuteTx(ctx, repository.db, nil, | |
| 69 | 70 | func(tx *gorm.DB) error { | |
| 71 | + if err := tx.WithContext(ctx).Where("id = ?", userID).First(user).Error; err != nil { | ||
| 72 | + return err | ||
| 73 | + } | ||
| 74 | + oldAPIKey = user.APIKey | ||
| 70 | 75 | return tx.WithContext(ctx).Model(user). | |
| 71 | 76 | Clauses(clause.Returning{}). | |
| 72 | 77 | Where("id = ?", userID). | |
@@ -78,6 +83,13 @@ func (repository *gormUserRepository) RotateAPIKey(ctx context.Context, userID e | |||
| 78 | 83 | return nil, repository.tracer.WrapErrorSpan(span, stacktrace.PropagateWithCode(err, ErrCodeNotFound, msg)) | |
| 79 | 84 | } | |
| 80 | 85 | ||
| 86 | + if err == nil && oldAPIKey != "" { | ||
| 87 | + // Flush pending ristretto Set operations before Del to avoid a | ||
| 88 | + // buffered Set re-adding the entry after removal. | ||
| 89 | + repository.cache.Wait() | ||
| 90 | + repository.cache.Del(oldAPIKey) | ||
| 91 | + } | ||
| 92 | + | ||
| 81 | 93 | return user, nil | |
| 82 | 94 | } | |
| 83 | 95 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -4,6 +4,7 @@ import ( | |||
| 4 | 4 | "bytes" | |
| 5 | 5 | "context" | |
| 6 | 6 | "encoding/json" | |
| 7 | + "fmt" | ||
| 7 | 8 | "io" | |
| 8 | 9 | "net/http" | |
| 9 | 10 | "strings" | |
@@ -213,6 +214,90 @@ func TestSendSMS_RateLimit(t *testing.T) { | |||
| 213 | 214 | } | |
| 214 | 215 | } | |
| 215 | 216 | ||
| 217 | + func TestRotateAPIKey_InvalidatesCache(t *testing.T) { | ||
| 218 | + ctx := context.Background() | ||
| 219 | + | ||
| 220 | + // Use a dedicated test user so we don't mutate the shared userAPIKey | ||
| 221 | + rotateUserAPIKey := "rotate-test-api-key" | ||
| 222 | + rotateUserID := "rotate-test-user-id" | ||
| 223 | + | ||
| 224 | + // 1) Confirm the dedicated user's API key works and warm the cache | ||
| 225 | + meURL := apiBaseURL + "/v1/users/me" | ||
| 226 | + req, err := http.NewRequestWithContext(ctx, http.MethodGet, meURL, nil) | ||
| 227 | + require.NoError(t, err) | ||
| 228 | + req.Header.Set("x-api-key", rotateUserAPIKey) | ||
| 229 | + | ||
| 230 | + resp, err := http.DefaultClient.Do(req) | ||
| 231 | + require.NoError(t, err) | ||
| 232 | + defer resp.Body.Close() | ||
| 233 | + | ||
| 234 | + body, err := io.ReadAll(resp.Body) | ||
| 235 | + require.NoError(t, err) | ||
| 236 | + require.Equal(t, http.StatusOK, resp.StatusCode, "initial auth failed: %s", string(body)) | ||
| 237 | + | ||
| 238 | + // Parse the current API key from the response | ||
| 239 | + var meResp struct { | ||
| 240 | + Data struct { | ||
| 241 | + ID string `json:"id"` | ||
| 242 | + APIKey string `json:"api_key"` | ||
| 243 | + } `json:"data"` | ||
| 244 | + } | ||
| 245 | + require.NoError(t, json.Unmarshal(body, &meResp)) | ||
| 246 | + require.Equal(t, rotateUserID, meResp.Data.ID) | ||
| 247 | + oldAPIKey := meResp.Data.APIKey | ||
| 248 | + require.NotEmpty(t, oldAPIKey) | ||
| 249 | + t.Logf("user ID: %s, old API key prefix: %s...", rotateUserID, oldAPIKey[:10]) | ||
| 250 | + | ||
| 251 | + // 2) Rotate the API key | ||
| 252 | + rotateURL := fmt.Sprintf("%s/v1/users/%s/api-keys", apiBaseURL, rotateUserID) | ||
| 253 | + req, err = http.NewRequestWithContext(ctx, http.MethodDelete, rotateURL, nil) | ||
| 254 | + require.NoError(t, err) | ||
| 255 | + req.Header.Set("x-api-key", rotateUserAPIKey) | ||
| 256 | + | ||
| 257 | + resp, err = http.DefaultClient.Do(req) | ||
| 258 | + require.NoError(t, err) | ||
| 259 | + defer resp.Body.Close() | ||
| 260 | + | ||
| 261 | + body, err = io.ReadAll(resp.Body) | ||
| 262 | + require.NoError(t, err) | ||
| 263 | + require.Equal(t, http.StatusOK, resp.StatusCode, "rotate failed: %s", string(body)) | ||
| 264 | + | ||
| 265 | + // Parse new API key from rotate response | ||
| 266 | + var rotateResp struct { | ||
| 267 | + Data struct { | ||
| 268 | + APIKey string `json:"api_key"` | ||
| 269 | + } `json:"data"` | ||
| 270 | + } | ||
| 271 | + require.NoError(t, json.Unmarshal(body, &rotateResp)) | ||
| 272 | + newAPIKey := rotateResp.Data.APIKey | ||
| 273 | + require.NotEmpty(t, newAPIKey) | ||
| 274 | + require.NotEqual(t, oldAPIKey, newAPIKey, "API key should have changed after rotation") | ||
| 275 | + t.Logf("new API key prefix: %s...", newAPIKey[:10]) | ||
| 276 | + | ||
| 277 | + // 3) Old API key should immediately fail (401) — this is the bug regression check | ||
| 278 | + req, err = http.NewRequestWithContext(ctx, http.MethodGet, meURL, nil) | ||
| 279 | + require.NoError(t, err) | ||
| 280 | + req.Header.Set("x-api-key", oldAPIKey) | ||
| 281 | + | ||
| 282 | + resp, err = http.DefaultClient.Do(req) | ||
| 283 | + require.NoError(t, err) | ||
| 284 | + defer resp.Body.Close() | ||
| 285 | + assert.Equal(t, http.StatusUnauthorized, resp.StatusCode, "old API key should return 401 after rotation") | ||
| 286 | + | ||
| 287 | + // 4) New API key should work | ||
| 288 | + req, err = http.NewRequestWithContext(ctx, http.MethodGet, meURL, nil) | ||
| 289 | + require.NoError(t, err) | ||
| 290 | + req.Header.Set("x-api-key", newAPIKey) | ||
| 291 | + | ||
| 292 | + resp, err = http.DefaultClient.Do(req) | ||
| 293 | + require.NoError(t, err) | ||
| 294 | + defer resp.Body.Close() | ||
| 295 | + | ||
| 296 | + body, err = io.ReadAll(resp.Body) | ||
| 297 | + require.NoError(t, err) | ||
| 298 | + assert.Equal(t, http.StatusOK, resp.StatusCode, "new API key should work: %s", string(body)) | ||
| 299 | + } | ||
| 300 | + | ||
| 216 | 301 | func TestSendSMS_OutstandingFlow(t *testing.T) { | |
| 217 | 302 | ctx := context.Background() | |
| 218 | 303 | phone := setupPhone(ctx, t, 60) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -13,6 +13,18 @@ VALUES ( | |||
| 13 | 13 | NOW() | |
| 14 | 14 | ) ON CONFLICT (id) DO NOTHING; | |
| 15 | 15 | ||
| 16 | + -- Test user for API key rotation tests (isolated to avoid mutating the shared test user) | ||
| 17 | + INSERT INTO users (id, email, api_key, timezone, subscription_name, created_at, updated_at) | ||
| 18 | + VALUES ( | ||
| 19 | + 'rotate-test-user-id', | ||
| 20 | + 'rotate-test@httpsms.com', | ||
| 21 | + 'rotate-test-api-key', | ||
| 22 | + 'UTC', | ||
| 23 | + 'pro-monthly', | ||
| 24 | + NOW(), | ||
| 25 | + NOW() | ||
| 26 | + ) ON CONFLICT (id) DO NOTHING; | ||
| 27 | + | ||
| 16 | 28 | -- System user (for event queue auth) | |
| 17 | 29 | INSERT INTO users (id, email, api_key, timezone, subscription_name, created_at, updated_at) | |
| 18 | 30 | VALUES ( | |
| Back | FazBrowse Home | New Git URL |
0 commit comments