-
Notifications
You must be signed in to change notification settings - Fork 0
Anonymous: Add configurable device limit #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: enhance-anonymous-access
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -13,10 +13,14 @@ import ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cacheKeyPrefix = "anon-device" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const anonymousDeviceExpiration = 30 * 24 * time.Hour | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var ErrDeviceLimitReached = fmt.Errorf("device limit reached") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type AnonDBStore struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sqlStore db.DB | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log log.Logger | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sqlStore db.DB | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| log log.Logger | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| deviceLimit int64 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type Device struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -45,8 +49,8 @@ type AnonStore interface { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DeleteDevicesOlderThan(ctx context.Context, olderThan time.Time) error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func ProvideAnonDBStore(sqlStore db.DB) *AnonDBStore { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return &AnonDBStore{sqlStore: sqlStore, log: log.New("anonstore")} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func ProvideAnonDBStore(sqlStore db.DB, deviceLimit int64) *AnonDBStore { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return &AnonDBStore{sqlStore: sqlStore, log: log.New("anonstore"), deviceLimit: deviceLimit} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (s *AnonDBStore) ListDevices(ctx context.Context, from *time.Time, to *time.Time) ([]*Device, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -65,9 +69,54 @@ func (s *AnonDBStore) ListDevices(ctx context.Context, from *time.Time, to *time | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return devices, err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func (s *AnonDBStore) CreateOrUpdateDevice(ctx context.Context, device *Device) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var query string | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // 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) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+109
to
+117
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Race condition: concurrent requests can bypass limit. Between
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: pkg/services/anonymous/anonimpl/anonstore/database.go
Line: 109:117
Comment:
**logic:** Race condition: concurrent requests can bypass limit. Between `CountDevices` and `updateDevice`, multiple goroutines can see count < limit and all proceed. Use database-level locking or atomic operations.
```suggestion
// if device limit is reached, only update devices
if s.deviceLimit > 0 {
// Try update first - if it succeeds, device exists
err := s.updateDevice(ctx, device)
if err == nil {
return nil
}
if !errors.Is(err, ErrDeviceLimitReached) {
return err
}
// Device doesn't exist - check limit with lock or use constraint
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 ErrDeviceLimitReached
}
}
```
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| args := []any{device.DeviceID, device.ClientIP, device.UserAgent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| device.CreatedAt.UTC(), device.UpdatedAt.UTC()} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| switch s.sqlStore.GetDBType() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,21 +2,20 @@ package anonimpl | |
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "net/http" | ||
| "strings" | ||
| "time" | ||
|
|
||
| "github.com/grafana/grafana/pkg/infra/log" | ||
| "github.com/grafana/grafana/pkg/services/anonymous" | ||
| "github.com/grafana/grafana/pkg/services/anonymous/anonimpl/anonstore" | ||
| "github.com/grafana/grafana/pkg/services/authn" | ||
| "github.com/grafana/grafana/pkg/services/org" | ||
| "github.com/grafana/grafana/pkg/setting" | ||
| ) | ||
|
|
||
| var _ authn.ContextAwareClient = new(Anonymous) | ||
|
|
||
| const timeoutTag = 2 * time.Minute | ||
|
|
||
| type Anonymous struct { | ||
| cfg *setting.Cfg | ||
| log log.Logger | ||
|
|
@@ -42,19 +41,13 @@ func (a *Anonymous) Authenticate(ctx context.Context, r *authn.Request) (*authn. | |
| httpReqCopy.RemoteAddr = r.HTTPRequest.RemoteAddr | ||
| } | ||
|
|
||
| go func() { | ||
| defer func() { | ||
| if err := recover(); err != nil { | ||
| a.log.Warn("Tag anon session panic", "err", err) | ||
| } | ||
| }() | ||
|
|
||
| newCtx, cancel := context.WithTimeout(context.Background(), timeoutTag) | ||
| defer cancel() | ||
| if err := a.anonDeviceService.TagDevice(newCtx, httpReqCopy, anonymous.AnonDeviceUI); err != nil { | ||
| a.log.Warn("Failed to tag anonymous session", "error", err) | ||
| if err := a.anonDeviceService.TagDevice(ctx, httpReqCopy, anonymous.AnonDeviceUI); err != nil { | ||
| if errors.Is(err, anonstore.ErrDeviceLimitReached) { | ||
| return nil, err | ||
| } | ||
| }() | ||
|
|
||
| a.log.Warn("Failed to tag anonymous session", "error", err) | ||
| } | ||
|
Comment on lines
+44
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Changed from async to blocking call without timeout. If Prompt To Fix With AIThis is a comment left during a code review.
Path: pkg/services/anonymous/anonimpl/client.go
Line: 44:50
Comment:
**style:** Changed from async to blocking call without timeout. If `TagDevice` is slow (DB contention during high traffic), authentication hangs. The old code had a 2-minute timeout.
How can I resolve this? If you propose a fix, please make it concise. |
||
|
|
||
| return &authn.Identity{ | ||
| ID: authn.AnonymousNamespaceID, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Flawed logic:
updateDeviceuses time-based condition that will fail for most update attempts. Whendevice.UpdatedAtis "now" and you checkupdated_at BETWEEN (now - 30 days) AND (now + 1 min), it only works if DB already has a recent entry. New devices or old devices won't match.The WHERE clause should only check
device_id:Prompt To Fix With AI