Skip to content

Anonymous: Add configurable device limit#2

Closed
ShashankFC wants to merge 1 commit into
enhance-anonymous-accessfrom
implement-device-limits
Closed

Anonymous: Add configurable device limit#2
ShashankFC wants to merge 1 commit into
enhance-anonymous-accessfrom
implement-device-limits

Conversation

@ShashankFC

@ShashankFC ShashankFC commented Dec 31, 2025

Copy link
Copy Markdown
Collaborator

Test 1nn

Summary by CodeRabbit

Release Notes

  • New Features
    • Added a new configurable anonymous device limit setting that allows fine-grained control over the maximum number of devices permitted to register with anonymous authentication
    • When the configured limit is reached, further registration attempts are automatically rejected and an appropriate error is returned to the requesting client

✏️ Tip: You can customize this high-level summary in your review settings.

nn---n*Replicated from [ai-code-review-evaluation/grafana-coderabbit#1](https://github.com/ai-code-review-evaluation/grafana-coderabbit/pull/1)*

* Anonymous: Add device limiter

* break auth if limit reached

* fix typo

* refactored const to make it clearer with expiration

* anon device limit for config

---------

Co-authored-by: Eric Leijonmarck <eric.leijonmarck@gmail.com>
@ShashankFC

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jan 12, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jan 12, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request introduces an anonymousDeviceLimit configuration property across the Grafana codebase. The feature adds device limit enforcement for anonymous users, implemented through configuration propagation from settings to frontend DTOs, and device-limit logic in the anonymous device store with proper error handling.

Changes

Cohort / File(s) Summary
Frontend Configuration Types
packages/grafana-data/src/types/config.ts, packages/grafana-runtime/src/config.ts
Added anonymousDeviceLimit: number | undefined property to frontend configuration interfaces (GrafanaConfig and GrafanaBootConfig)
Backend Configuration & Frontend Settings
pkg/setting/setting.go, pkg/api/dtos/frontend_settings.go, pkg/api/frontendsettings.go
Added AnonymousDeviceLimit field to Cfg struct, FrontendSettingsDTO, and populated it from backend config during frontend settings construction
Anonymous Device Store
pkg/services/anonymous/anonimpl/anonstore/database.go
Introduced device expiration constant, new ErrDeviceLimitReached error, added deviceLimit field to AnonDBStore, updated constructor signature, implemented updateDevice method, and modified CreateOrUpdateDevice to enforce device limits by reusing existing devices when limit is reached
Anonymous Device Store Tests
pkg/services/anonymous/anonimpl/anonstore/database_test.go
Updated existing test calls to pass device limit parameter (0 and 1), added TestIntegrationBeyondDeviceLimit to verify device-limit enforcement behavior
Anonymous Device API & Service Wiring
pkg/services/anonymous/anonimpl/api/api.go, pkg/services/anonymous/anonimpl/client.go, pkg/services/anonymous/anonimpl/impl.go, pkg/services/anonymous/anonimpl/impl_test.go
Renamed constant from thirtyDays to anonymousDeviceExpiration, replaced timeout-based tagging with direct context usage, updated error handling to propagate ErrDeviceLimitReached, refactored dependency injection to accept sqlStore instead of anonStore and initialize via constructor, updated tests to reflect new wiring

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant Service as AnonDeviceService
    participant Store as AnonDBStore
    participant DB as Database

    Client->>Service: TagDevice(ctx, device)
    Service->>Store: CreateOrUpdateDevice(ctx, device)
    
    alt Device Limit Disabled (limit ≤ 0)
        Store->>DB: INSERT new device
        DB-->>Store: Success
    else Device Limit Enabled (limit > 0)
        Store->>DB: COUNT devices within expiration window
        DB-->>Store: deviceCount
        
        alt Count < Limit
            Store->>DB: INSERT new device
            DB-->>Store: Success
        else Count ≥ Limit
            Store->>Store: updateDevice(existing device)
            Store->>DB: UPDATE device (if recently updated)
            DB-->>Store: Result (0 or 1 rows)
            
            alt Rows Updated = 0
                Store-->>Service: ErrDeviceLimitReached
                Service-->>Client: Error
            else Rows Updated = 1
                Store-->>Service: Success
                Service-->>Client: Device Updated
            end
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes


🐰 A limit for the anonymous horde,
Device counts now kept in store.
When the quota's met with care,
Updates reuse without despair.
Configuration flows from top to ground,
Enforcing limits all around!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Anonymous: Add configurable device limit' accurately summarizes the main change—adding a configurable device limit feature to the anonymous authentication system across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
packages/grafana-runtime/src/config.ts (1)

97-97: Add explicit type annotation for clarity and interface compliance.

The property is missing an explicit type annotation. While TypeScript will infer the type as undefined, the GrafanaConfig interface defines this as number | undefined. Adding the type annotation ensures type safety and makes the code more self-documenting.

Suggested fix
-  anonymousDeviceLimit = undefined;
+  anonymousDeviceLimit: number | undefined = undefined;
pkg/services/anonymous/anonimpl/api/api.go (1)

18-18: Consider extracting the duplicated constant to a shared location.

The anonymousDeviceExpiration constant is defined identically in both this file and pkg/services/anonymous/anonimpl/anonstore/database.go. This duplication risks inconsistency if one value is updated without the other.

Consider moving this constant to a shared package (e.g., the anonstore package) and importing it here.

Suggested approach

In anonstore/database.go, export the constant:

-const anonymousDeviceExpiration = 30 * 24 * time.Hour
+const AnonymousDeviceExpiration = 30 * 24 * time.Hour

Then in api/api.go:

-const anonymousDeviceExpiration = 30 * 24 * time.Hour
+// Use the shared constant from anonstore

And reference anonstore.AnonymousDeviceExpiration in both locations.

pkg/services/anonymous/anonimpl/anonstore/database_test.go (1)

51-70: Test correctly verifies device limit enforcement.

The test properly validates that creating a second device with a different DeviceID fails with ErrDeviceLimitReached when the limit is 1.

Consider adding a test case to verify that updating an existing device (same DeviceID) still succeeds after the limit is reached—this ensures the limit only blocks new devices, not updates to existing ones.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ed86583 and 3647ba7.

📒 Files selected for processing (11)
  • packages/grafana-data/src/types/config.ts
  • packages/grafana-runtime/src/config.ts
  • pkg/api/dtos/frontend_settings.go
  • pkg/api/frontendsettings.go
  • pkg/services/anonymous/anonimpl/anonstore/database.go
  • pkg/services/anonymous/anonimpl/anonstore/database_test.go
  • pkg/services/anonymous/anonimpl/api/api.go
  • pkg/services/anonymous/anonimpl/client.go
  • pkg/services/anonymous/anonimpl/impl.go
  • pkg/services/anonymous/anonimpl/impl_test.go
  • pkg/setting/setting.go
🧰 Additional context used
🧬 Code graph analysis (4)
pkg/services/anonymous/anonimpl/anonstore/database_test.go (1)
pkg/services/anonymous/anonimpl/anonstore/database.go (3)
  • ProvideAnonDBStore (52-54)
  • Device (26-33)
  • ErrDeviceLimitReached (18-18)
pkg/api/frontendsettings.go (1)
pkg/setting/setting.go (1)
  • Cfg (149-578)
pkg/services/anonymous/anonimpl/impl.go (4)
pkg/setting/setting.go (1)
  • Cfg (149-578)
pkg/infra/serverlock/serverlock.go (1)
  • ServerLockService (27-31)
pkg/services/anonymous/anonimpl/anonstore/database.go (1)
  • ProvideAnonDBStore (52-54)
pkg/services/anonymous/anonimpl/api/api.go (1)
  • NewAnonDeviceServiceAPI (34-47)
pkg/services/anonymous/anonimpl/client.go (2)
pkg/services/anonymous/service.go (1)
  • AnonDeviceUI (12-12)
pkg/services/anonymous/anonimpl/anonstore/database.go (1)
  • ErrDeviceLimitReached (18-18)
🔇 Additional comments (14)
packages/grafana-data/src/types/config.ts (1)

199-201: LGTM!

The new anonymousDeviceLimit property is correctly typed as number | undefined and logically placed alongside the related anonymousEnabled setting.

pkg/setting/setting.go (2)

371-375: LGTM!

The AnonymousDeviceLimit field is correctly placed alongside other anonymous configuration fields and uses int64 type consistent with similar settings in the codebase.


1650-1655: LGTM!

The configuration parsing is clean. Refactoring to use the anonSection variable improves readability, and the default value of 0 for device_limit correctly means "no limit" based on the enforcement logic in the store layer.

pkg/services/anonymous/anonimpl/anonstore/database.go (1)

80-81: Verify the upper bound time window logic.

The upper bound device.UpdatedAt.UTC().Add(time.Minute) adds 1 minute to the device's update time. This appears to be a small buffer, but could you clarify the reasoning? If device.UpdatedAt is set to time.Now() before calling this method, the buffer may be unnecessary or could mask timing issues.

pkg/api/frontendsettings.go (1)

197-198: LGTM!

The AnonymousDeviceLimit is correctly sourced from the configuration and positioned logically alongside AnonymousEnabled.

pkg/api/dtos/frontend_settings.go (1)

194-196: LGTM!

The AnonymousDeviceLimit field is correctly typed as int64 with proper JSON serialization tag, maintaining consistency with the backend configuration.

pkg/services/anonymous/anonimpl/api/api.go (1)

70-72: LGTM!

The usage of the renamed constant is correct and maintains the existing behavior.

pkg/services/anonymous/anonimpl/anonstore/database_test.go (1)

16-16: LGTM!

Passing 0 as the device limit preserves the original behavior (unlimited devices) for existing tests.

pkg/services/anonymous/anonimpl/impl_test.go (2)

116-117: Wiring change aligns with the new service initialization pattern.

The test now passes the DB store directly to ProvideAnonymousDeviceService, which internally creates the anonStore with the configured device limit from cfg.AnonymousDeviceLimit. Since setting.NewCfg() defaults AnonymousDeviceLimit to 0 (unlimited), existing test behavior is preserved.


124-124: Accessing internal anonStore field for testing.

This works for integration tests but couples the test to internal implementation details. If the anonStore field becomes unexported or renamed, tests will break.

This is acceptable for now given the test's integration nature, but consider exposing a test helper or interface method if this access pattern expands.

pkg/services/anonymous/anonimpl/impl.go (3)

36-43: Good refactor to encapsulate store creation within the service.

The signature change from accepting an anonStore to accepting sqlStore db.DB and creating the store internally with cfg.AnonymousDeviceLimit improves encapsulation. The device limit configuration is now properly wired from the Grafana config.


144-148: Error propagation change is essential for device limit enforcement.

Previously, errors from tagDeviceUI were logged but swallowed. Now they're returned, which is necessary for ErrDeviceLimitReached to propagate up to client.go and block authentication.

Note: For non-limit errors (e.g., transient DB issues), this will now also block authentication where it previously allowed anonymous access to proceed. Verify this is the intended behavior—if not, consider only returning ErrDeviceLimitReached and continuing on other errors (similar to the pattern in client.go).


61-62: API wiring correctly uses the internally created store.

The anonAPI now receives a.anonStore which is the DB-backed store with the device limit configuration applied.

pkg/services/anonymous/anonimpl/client.go (1)

44-50: Error handling for device limit is correct and properly propagated.

The use of errors.Is() for sentinel error comparison is idiomatic Go. The error is properly returned from Authenticate() and propagated up through the authentication middleware, where it blocks authentication as intended.

The middleware in pkg/services/accesscontrol/middleware.go handles LookupTokenErr (which includes ErrDeviceLimitReached) by returning HTTP 401 Unauthorized for API requests. While 429 Too Many Requests would be semantically more precise for rate limiting, HTTP 401 is acceptable for blocking authentication. No additional error handling is needed.

Comment on lines +72 to +103
// updateDevice updates a device if it exists and has been updated between the given times.
func (s *AnonDBStore) updateDevice(ctx context.Context, device *Device) error {
const query = `UPDATE anon_device SET
client_ip = ?,
user_agent = ?,
updated_at = ?
WHERE device_id = ? AND updated_at BETWEEN ? AND ?`

args := []interface{}{device.ClientIP, device.UserAgent, device.UpdatedAt.UTC(), device.DeviceID,
device.UpdatedAt.UTC().Add(-anonymousDeviceExpiration), device.UpdatedAt.UTC().Add(time.Minute),
}
err := s.sqlStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
args = append([]interface{}{query}, args...)
result, err := dbSession.Exec(args...)
if err != nil {
return err
}

rowsAffected, err := result.RowsAffected()
if err != nil {
return err
}

if rowsAffected == 0 {
return ErrDeviceLimitReached
}

return nil
})

return err
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify error semantics: rowsAffected == 0 may not always indicate limit reached.

The updateDevice method returns ErrDeviceLimitReached when no rows are affected, but this could also occur if the device doesn't exist or if updated_at falls outside the time window for reasons other than the limit being reached. This conflates different failure modes.

Consider returning a more specific error or adding a check to distinguish between "device not found" and "device limit reached".

Suggested approach
 		if rowsAffected == 0 {
-			return ErrDeviceLimitReached
+			return ErrDeviceLimitReached // Note: This also triggers if device doesn't exist within the expiration window
 		}

Alternatively, you could query for the device's existence first to provide a more accurate error, though this adds an extra query.

Comment on lines +108 to +118
// if device limit is reached, only update devices
if s.deviceLimit > 0 {
count, err := s.CountDevices(ctx, time.Now().UTC().Add(-anonymousDeviceExpiration), time.Now().UTC().Add(time.Minute))
if err != nil {
return err
}

if count >= s.deviceLimit {
return s.updateDevice(ctx, device)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the full function containing lines 108-118
cd /root/repo && sed -n '80,130p' pkg/services/anonymous/anonimpl/anonstore/database.go

Repository: AI-Code-Review-Evals/coderabbit-grafana

Length of output: 143


🏁 Script executed:

# Let's also check the file size to ensure we can read it safely
wc -l pkg/services/anonymous/anonimpl/anonstore/database.go

Repository: AI-Code-Review-Evals/coderabbit-grafana

Length of output: 140


🏁 Script executed:

# Search for related comments about device limit enforcement
rg -i "device.*limit" pkg/services/anonymous/ -A 2 -B 2

Repository: AI-Code-Review-Evals/coderabbit-grafana

Length of output: 4253


🏁 Script executed:

# Look for transaction usage in this file
rg -i "transaction|begin|commit|rollback" pkg/services/anonymous/anonimpl/anonstore/database.go

Repository: AI-Code-Review-Evals/coderabbit-grafana

Length of output: 65


🏁 Script executed:

cat -n pkg/services/anonymous/anonimpl/anonstore/database.go

Repository: AI-Code-Review-Evals/coderabbit-grafana

Length of output: 7419


🏁 Script executed:

# Also check the test to understand expected behavior
cat -n pkg/services/anonymous/anonimpl/anonstore/database_test.go

Repository: AI-Code-Review-Evals/coderabbit-grafana

Length of output: 3519


TOCTOU race condition between count check and insert—device limit can be exceeded under concurrent load.

The count check (line 110) and subsequent insert/update operations (lines 115-155) are not atomic. Multiple concurrent requests can both pass the count check simultaneously and then both proceed to insert, exceeding the device limit. For example, with a limit of 1: Request A counts 0 devices, Request B counts 0 devices, then both insert, resulting in 2 devices.

Since ErrDeviceLimitReached is returned as an explicit error, this appears to be intended as a strict limit. To properly enforce this, wrap the count check and insert in a database transaction with row locking (e.g., SELECT ... FOR UPDATE) or use a unique constraint on a computed "active device slot" to prevent overcounting at the database level.

@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been automatically marked as stale because it has not had activity in the last 30 days. It will be closed in 2 weeks if no further activity occurs. Please feel free to give a status update or ping for review. Thank you for your contributions!

@github-actions github-actions Bot added the stale label Feb 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This pull request has been automatically closed because it has not had any further activity in the last 2 weeks. Thank you for your contributions!

@github-actions github-actions Bot closed this Feb 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants