Anon: Scaffold anon service (#74744)

* remove API tagging method and authed tagging

* add anonstore

move debug to after cache

change test order

fix issue where mysql trims to second

* add old device cleanup

lint

utc-ize everything

trim whitespace

* remove dangling setting

* Add delete devices

* Move anonymous authnclient to anonimpl

* Add simple post login hook

* move registration of Background Service

cleanup

* add updated_at index

* do not untag device if login err

* add delete device integration test
This commit is contained in:
Jo
2023-09-25 16:25:29 +02:00
committed by GitHub
parent 6f665b0901
commit 40a1f8434d
16 changed files with 429 additions and 434 deletions

View File

@@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats/statscollector" "github.com/grafana/grafana/pkg/infra/usagestats/statscollector"
"github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/anonymous/anonimpl"
"github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/cleanup" "github.com/grafana/grafana/pkg/services/cleanup"
"github.com/grafana/grafana/pkg/services/dashboardsnapshots" "github.com/grafana/grafana/pkg/services/dashboardsnapshots"
@@ -54,6 +55,7 @@ func ProvideBackgroundServiceRegistry(
publicDashboardsMetric *publicdashboardsmetric.Service, publicDashboardsMetric *publicdashboardsmetric.Service,
keyRetriever *dynamic.KeyRetriever, keyRetriever *dynamic.KeyRetriever,
dynamicAngularDetectorsProvider *angulardetectorsprovider.Dynamic, dynamicAngularDetectorsProvider *angulardetectorsprovider.Dynamic,
anon *anonimpl.AnonDeviceService,
// Need to make sure these are initialized, is there a better place to put them? // Need to make sure these are initialized, is there a better place to put them?
_ dashboardsnapshots.Service, _ *alerting.AlertNotificationService, _ dashboardsnapshots.Service, _ *alerting.AlertNotificationService,
_ serviceaccounts.Service, _ *guardian.Provider, _ serviceaccounts.Service, _ *guardian.Provider,
@@ -92,6 +94,7 @@ func ProvideBackgroundServiceRegistry(
publicDashboardsMetric, publicDashboardsMetric,
keyRetriever, keyRetriever,
dynamicAngularDetectorsProvider, dynamicAngularDetectorsProvider,
anon,
) )
} }

View File

@@ -8,6 +8,7 @@ package server
import ( import (
"github.com/google/wire" "github.com/google/wire"
sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
"github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/api"
@@ -39,6 +40,7 @@ import (
"github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/annotations"
"github.com/grafana/grafana/pkg/services/annotations/annotationsimpl" "github.com/grafana/grafana/pkg/services/annotations/annotationsimpl"
"github.com/grafana/grafana/pkg/services/anonymous/anonimpl/anonstore"
"github.com/grafana/grafana/pkg/services/apikey/apikeyimpl" "github.com/grafana/grafana/pkg/services/apikey/apikeyimpl"
"github.com/grafana/grafana/pkg/services/auth/jwt" "github.com/grafana/grafana/pkg/services/auth/jwt"
"github.com/grafana/grafana/pkg/services/authn/authnimpl" "github.com/grafana/grafana/pkg/services/authn/authnimpl"
@@ -351,6 +353,8 @@ var wireBasicSet = wire.NewSet(
authnimpl.ProvideAuthnService, authnimpl.ProvideAuthnService,
supportbundlesimpl.ProvideService, supportbundlesimpl.ProvideService,
oasimpl.ProvideService, oasimpl.ProvideService,
anonstore.ProvideAnonDBStore,
wire.Bind(new(anonstore.AnonStore), new(*anonstore.AnonDBStore)),
wire.Bind(new(oauthserver.OAuth2Server), new(*oasimpl.OAuth2ServiceImpl)), wire.Bind(new(oauthserver.OAuth2Server), new(*oasimpl.OAuth2ServiceImpl)),
loggermw.Provide, loggermw.Provide,
signingkeysimpl.ProvideEmbeddedSigningKeysService, signingkeysimpl.ProvideEmbeddedSigningKeysService,

View File

@@ -0,0 +1,126 @@
package anonstore
import (
"context"
"fmt"
"strings"
"time"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
)
const cacheKeyPrefix = "anon-device"
type AnonDBStore struct {
sqlStore db.DB
log log.Logger
}
type Device struct {
ID int64 `json:"-" db:"id"`
DeviceID string `json:"device_id" db:"device_id"`
ClientIP string `json:"client_ip" db:"client_ip"`
UserAgent string `json:"user_agent" db:"user_agent"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
func (a *Device) CacheKey() string {
return strings.Join([]string{cacheKeyPrefix, a.DeviceID}, ":")
}
type AnonStore interface {
// ListDevices returns all devices that have been updated between the given times.
ListDevices(ctx context.Context, from *time.Time, to *time.Time) ([]*Device, error)
// CreateOrUpdateDevice creates or updates a device.
CreateOrUpdateDevice(ctx context.Context, device *Device) error
// CountDevices returns the number of devices that have been updated between the given times.
CountDevices(ctx context.Context, from time.Time, to time.Time) (int64, error)
// DeleteDevice deletes a device by its ID.
DeleteDevice(ctx context.Context, deviceID string) error
// DeleteDevicesOlderThan deletes all devices that have no been updated since the given time.
DeleteDevicesOlderThan(ctx context.Context, olderThan time.Time) error
}
func ProvideAnonDBStore(sqlStore db.DB) *AnonDBStore {
return &AnonDBStore{sqlStore: sqlStore, log: log.New("anonstore")}
}
func (s *AnonDBStore) ListDevices(ctx context.Context, from *time.Time, to *time.Time) ([]*Device, error) {
devices := []*Device{}
query := "SELECT * FROM anon_device"
args := []any{}
if from != nil && to != nil {
query += " WHERE updated_at BETWEEN ? AND ?"
args = append(args, from.UTC(), to.UTC())
}
err := s.sqlStore.GetSqlxSession().Select(ctx, &devices, query, args...)
if err != nil {
return nil, err
}
return devices, nil
}
func (s *AnonDBStore) CreateOrUpdateDevice(ctx context.Context, device *Device) error {
var query string
args := []any{device.DeviceID, device.ClientIP, device.UserAgent,
device.CreatedAt.UTC(), device.UpdatedAt.UTC()}
switch s.sqlStore.GetDBType() {
case migrator.Postgres:
query = `INSERT INTO anon_device (device_id, client_ip, user_agent, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (device_id) DO UPDATE SET
client_ip = $2,
user_agent = $3,
updated_at = $5
RETURNING id`
case migrator.MySQL:
query = `INSERT INTO anon_device (device_id, client_ip, user_agent, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
client_ip = VALUES(client_ip),
user_agent = VALUES(user_agent),
updated_at = VALUES(updated_at)`
case migrator.SQLite:
query = `INSERT INTO anon_device (device_id, client_ip, user_agent, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT (device_id) DO UPDATE SET
client_ip = excluded.client_ip,
user_agent = excluded.user_agent,
updated_at = excluded.updated_at`
default:
return fmt.Errorf("unsupported database driver: %s", s.sqlStore.GetDBType())
}
_, err := s.sqlStore.GetSqlxSession().Exec(ctx, query, args...)
return err
}
func (s *AnonDBStore) CountDevices(ctx context.Context, from time.Time, to time.Time) (int64, error) {
var count int64
err := s.sqlStore.GetSqlxSession().Get(ctx, &count, "SELECT COUNT(*) FROM anon_device WHERE updated_at BETWEEN ? AND ?", from.UTC(), to.UTC())
if err != nil {
return 0, err
}
return count, nil
}
func (s *AnonDBStore) DeleteDevice(ctx context.Context, deviceID string) error {
_, err := s.sqlStore.GetSqlxSession().Exec(ctx, "DELETE FROM anon_device WHERE device_id = ?", deviceID)
if err != nil {
return err
}
return nil
}
// deleteOldDevices deletes all devices that have no been updated since the given time.
func (s *AnonDBStore) DeleteDevicesOlderThan(ctx context.Context, olderThan time.Time) error {
_, err := s.sqlStore.GetSqlxSession().Exec(ctx, "DELETE FROM anon_device WHERE updated_at <= ?", olderThan.UTC())
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,79 @@
package anonstore
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/db"
)
func TestIntegrationAnonStore_DeleteDevicesOlderThan(t *testing.T) {
store := db.InitTestDB(t)
anonDBStore := ProvideAnonDBStore(store)
const keepFor = time.Hour * 24 * 61
anonDevice := &Device{
DeviceID: "32mdo31deeqwes",
ClientIP: "10.30.30.2",
UserAgent: "test",
UpdatedAt: time.Now().Add(-keepFor).Add(-time.Hour),
}
err := anonDBStore.CreateOrUpdateDevice(context.Background(), anonDevice)
require.NoError(t, err)
anonDevice.DeviceID = "keep"
anonDevice.UpdatedAt = time.Now().Add(-time.Hour)
err = anonDBStore.CreateOrUpdateDevice(context.Background(), anonDevice)
require.NoError(t, err)
from := time.Now().Add(-2 * keepFor)
to := time.Now()
count, err := anonDBStore.CountDevices(context.Background(), from, to)
require.NoError(t, err)
require.Equal(t, int64(2), count)
err = anonDBStore.DeleteDevicesOlderThan(context.Background(), time.Now().Add(-keepFor))
require.NoError(t, err)
devices, err := anonDBStore.ListDevices(context.Background(), &from, &to)
require.NoError(t, err)
require.Equal(t, 1, len(devices))
assert.Equal(t, "keep", devices[0].DeviceID)
}
func TestIntegrationAnonStore_DeleteDevice(t *testing.T) {
store := db.InitTestDB(t)
anonDBStore := ProvideAnonDBStore(store)
const keepFor = time.Hour * 24 * 61
anonDevice := &Device{
DeviceID: "32mdo31deeqwes",
ClientIP: "10.30.30.2",
UserAgent: "test",
UpdatedAt: time.Now().Add(-keepFor).Add(-time.Hour),
}
err := anonDBStore.CreateOrUpdateDevice(context.Background(), anonDevice)
require.NoError(t, err)
from := time.Now().Add(-2 * keepFor)
to := time.Now()
count, err := anonDBStore.CountDevices(context.Background(), from, to)
require.NoError(t, err)
require.Equal(t, int64(1), count)
err = anonDBStore.DeleteDevice(context.Background(), "32mdo31deeqwes")
require.NoError(t, err)
devices, err := anonDBStore.ListDevices(context.Background(), &from, &to)
require.NoError(t, err)
require.Equal(t, 0, len(devices))
}

View File

@@ -0,0 +1,21 @@
package anonstore
import (
"context"
"time"
)
type FakeAnonStore struct {
}
func (s *FakeAnonStore) ListDevices(ctx context.Context, from *time.Time, to *time.Time) ([]*Device, error) {
return nil, nil
}
func (s *FakeAnonStore) CreateOrUpdateDevice(ctx context.Context, device *Device) error {
return nil
}
func (s *FakeAnonStore) CountDevices(ctx context.Context, from time.Time, to time.Time) (int64, error) {
return 0, nil
}

View File

@@ -1,4 +1,4 @@
package clients package anonimpl
import ( import (
"context" "context"
@@ -17,15 +17,6 @@ var _ authn.ContextAwareClient = new(Anonymous)
const timeoutTag = 2 * time.Minute const timeoutTag = 2 * time.Minute
func ProvideAnonymous(cfg *setting.Cfg, orgService org.Service, anonDeviceService anonymous.Service) *Anonymous {
return &Anonymous{
cfg: cfg,
log: log.New("authn.anonymous"),
orgService: orgService,
anonDeviceService: anonDeviceService,
}
}
type Anonymous struct { type Anonymous struct {
cfg *setting.Cfg cfg *setting.Cfg
log log.Logger log log.Logger
@@ -60,7 +51,7 @@ func (a *Anonymous) Authenticate(ctx context.Context, r *authn.Request) (*authn.
newCtx, cancel := context.WithTimeout(context.Background(), timeoutTag) newCtx, cancel := context.WithTimeout(context.Background(), timeoutTag)
defer cancel() defer cancel()
if err := a.anonDeviceService.TagDevice(newCtx, httpReqCopy, anonymous.AnonDevice); err != nil { if err := a.anonDeviceService.TagDevice(newCtx, httpReqCopy, anonymous.AnonDeviceUI); err != nil {
a.log.Warn("Failed to tag anonymous session", "error", err) a.log.Warn("Failed to tag anonymous session", "error", err)
} }
}() }()

View File

@@ -1,4 +1,4 @@
package clients package anonimpl
import ( import (
"context" "context"

View File

@@ -2,144 +2,76 @@ package anonimpl
import ( import (
"context" "context"
"encoding/hex"
"encoding/json"
"fmt"
"hash/fnv"
"net/http" "net/http"
"strings"
"time" "time"
"github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/localcache"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/network" "github.com/grafana/grafana/pkg/infra/network"
"github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/services/anonymous" "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" "github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web" "github.com/grafana/grafana/pkg/web"
) )
const thirtyDays = 30 * 24 * time.Hour const thirtyDays = 30 * 24 * time.Hour
const deviceIDHeader = "X-Grafana-Device-Id" const deviceIDHeader = "X-Grafana-Device-Id"
const keepFor = time.Hour * 24 * 61
type Device struct {
Kind anonymous.DeviceKind `json:"kind"`
IP string `json:"ip"`
UserAgent string `json:"user_agent"`
LastSeen time.Time `json:"last_seen"`
}
func (a *Device) Key() (string, error) {
key := strings.Builder{}
key.WriteString(a.IP)
key.WriteString(a.UserAgent)
hash := fnv.New128a()
if _, err := hash.Write([]byte(key.String())); err != nil {
return "", fmt.Errorf("failed to write to hash: %w", err)
}
return strings.Join([]string{string(a.Kind), hex.EncodeToString(hash.Sum(nil))}, ":"), nil
}
func (a *Device) UIKey(deviceID string) (string, error) {
return strings.Join([]string{string(a.Kind), deviceID}, ":"), nil
}
type AnonDeviceService struct { type AnonDeviceService struct {
remoteCache remotecache.CacheStorage log log.Logger
log log.Logger localCache *localcache.CacheService
localCache *localcache.CacheService anonStore anonstore.AnonStore
serverLock *serverlock.ServerLockService
} }
func ProvideAnonymousDeviceService(remoteCache remotecache.CacheStorage, usageStats usagestats.Service) *AnonDeviceService { func ProvideAnonymousDeviceService(usageStats usagestats.Service, authBroker authn.Service,
anonStore anonstore.AnonStore, cfg *setting.Cfg, orgService org.Service,
serverLockService *serverlock.ServerLockService,
) *AnonDeviceService {
a := &AnonDeviceService{ a := &AnonDeviceService{
remoteCache: remoteCache, log: log.New("anonymous-session-service"),
log: log.New("anonymous-session-service"), localCache: localcache.New(29*time.Minute, 15*time.Minute),
localCache: localcache.New(29*time.Minute, 15*time.Minute), anonStore: anonStore,
serverLock: serverLockService,
} }
usageStats.RegisterMetricsFunc(a.usageStatFn) usageStats.RegisterMetricsFunc(a.usageStatFn)
anonClient := &Anonymous{
cfg: cfg,
log: log.New("authn.anonymous"),
orgService: orgService,
anonDeviceService: a,
}
if anonClient.cfg.AnonymousEnabled {
authBroker.RegisterClient(anonClient)
authBroker.RegisterPostLoginHook(a.untagDevice, 100)
}
return a return a
} }
func (a *AnonDeviceService) usageStatFn(ctx context.Context) (map[string]any, error) { func (a *AnonDeviceService) usageStatFn(ctx context.Context) (map[string]any, error) {
anonDeviceCount, err := a.remoteCache.Count(ctx, string(anonymous.AnonDevice)) // Count the number of unique devices that have been updated in the last 30 days.
// One minute is added to the end time as mysql has a precision of seconds and it will break tests that write too fast.
anonUIDeviceCount, err := a.anonStore.CountDevices(ctx, time.Now().Add(-thirtyDays), time.Now().Add(time.Minute))
if err != nil { if err != nil {
return nil, nil return nil, err
}
authedDeviceCount, err := a.remoteCache.Count(ctx, string(anonymous.AuthedDevice))
if err != nil {
return nil, nil
}
anonUIDeviceCount, err := a.remoteCache.Count(ctx, string(anonymous.AnonDeviceUI))
if err != nil {
return nil, nil
}
authedUIDeviceCount, err := a.remoteCache.Count(ctx, string(anonymous.AuthedDeviceUI))
if err != nil {
return nil, nil
} }
return map[string]any{ return map[string]any{
"stats.anonymous.session.count": anonDeviceCount, // keep session for legacy data
"stats.users.device.count": authedDeviceCount,
"stats.anonymous.device.ui.count": anonUIDeviceCount, "stats.anonymous.device.ui.count": anonUIDeviceCount,
"stats.users.device.ui.count": authedUIDeviceCount,
}, nil }, nil
} }
func (a *AnonDeviceService) untagDevice(ctx context.Context, device *Device) error { func (a *AnonDeviceService) tagDeviceUI(ctx context.Context, httpReq *http.Request, device *anonstore.Device) error {
key, err := device.Key() key := device.CacheKey()
if err != nil {
return err
}
if err := a.remoteCache.Delete(ctx, key); err != nil {
return err
}
return nil
}
func (a *AnonDeviceService) untagUIDevice(ctx context.Context, deviceID string, device *Device) error {
key, err := device.UIKey(deviceID)
if err != nil {
return err
}
if err := a.remoteCache.Delete(ctx, key); err != nil {
return err
}
return nil
}
func (a *AnonDeviceService) tagDeviceUI(ctx context.Context, httpReq *http.Request, device Device) error {
deviceID := httpReq.Header.Get(deviceIDHeader)
if deviceID == "" {
return nil
}
if device.Kind == anonymous.AnonDevice {
device.Kind = anonymous.AnonDeviceUI
} else if device.Kind == anonymous.AuthedDevice {
device.Kind = anonymous.AuthedDeviceUI
}
key, err := device.UIKey(deviceID)
if err != nil {
return err
}
if setting.Env == setting.Dev {
a.log.Debug("Tagging device for UI", "deviceID", deviceID, "device", device, "key", key)
}
if _, ok := a.localCache.Get(key); ok { if _, ok := a.localCache.Get(key); ok {
return nil return nil
@@ -147,33 +79,41 @@ func (a *AnonDeviceService) tagDeviceUI(ctx context.Context, httpReq *http.Reque
a.localCache.SetDefault(key, struct{}{}) a.localCache.SetDefault(key, struct{}{})
deviceJSON, err := json.Marshal(device) if setting.Env == setting.Dev {
if err != nil { a.log.Debug("Tagging device for UI", "deviceID", device.DeviceID, "device", device, "key", key)
return err
} }
if err := a.remoteCache.Set(ctx, key, deviceJSON, thirtyDays); err != nil { if err := a.anonStore.CreateOrUpdateDevice(ctx, device); err != nil {
return err
}
// remove existing tag when device switches to another kind
untagKind := anonymous.AnonDeviceUI
if device.Kind == anonymous.AnonDeviceUI {
untagKind = anonymous.AuthedDeviceUI
}
if err := a.untagUIDevice(ctx, deviceID, &Device{
Kind: untagKind,
IP: device.IP,
UserAgent: device.UserAgent,
}); err != nil {
return err return err
} }
return nil return nil
} }
func (a *AnonDeviceService) untagDevice(ctx context.Context,
identity *authn.Identity, r *authn.Request, err error) {
if err != nil {
return
}
deviceID := r.HTTPRequest.Header.Get(deviceIDHeader)
if deviceID == "" {
return
}
errD := a.anonStore.DeleteDevice(ctx, deviceID)
if errD != nil {
a.log.Debug("Failed to untag device", "error", err)
}
}
// FIXME: Unexport and remove interface
func (a *AnonDeviceService) TagDevice(ctx context.Context, httpReq *http.Request, kind anonymous.DeviceKind) error { func (a *AnonDeviceService) TagDevice(ctx context.Context, httpReq *http.Request, kind anonymous.DeviceKind) error {
deviceID := httpReq.Header.Get(deviceIDHeader)
if deviceID == "" {
return nil
}
addr := web.RemoteAddr(httpReq) addr := web.RemoteAddr(httpReq)
ip, err := network.GetIPFromAddress(addr) ip, err := network.GetIPFromAddress(addr)
if err != nil { if err != nil {
@@ -186,54 +126,39 @@ func (a *AnonDeviceService) TagDevice(ctx context.Context, httpReq *http.Request
clientIPStr = "" clientIPStr = ""
} }
taggedDevice := &Device{ taggedDevice := &anonstore.Device{
Kind: kind, DeviceID: deviceID,
IP: clientIPStr, ClientIP: clientIPStr,
UserAgent: httpReq.UserAgent(), UserAgent: httpReq.UserAgent(),
LastSeen: time.Now().UTC(), CreatedAt: time.Now(),
UpdatedAt: time.Now(),
} }
err = a.tagDeviceUI(ctx, httpReq, *taggedDevice) err = a.tagDeviceUI(ctx, httpReq, taggedDevice)
if err != nil { if err != nil {
a.log.Debug("Failed to tag device for UI", "error", err) a.log.Debug("Failed to tag device for UI", "error", err)
} }
key, err := taggedDevice.Key()
if err != nil {
return err
}
if setting.Env == setting.Dev {
a.log.Debug("Tagging device", "device", taggedDevice, "key", key)
}
if _, ok := a.localCache.Get(key); ok {
return nil
}
a.localCache.SetDefault(key, struct{}{})
deviceJSON, err := json.Marshal(taggedDevice)
if err != nil {
return err
}
if err := a.remoteCache.Set(ctx, key, deviceJSON, thirtyDays); err != nil {
return err
}
// remove existing tag when device switches to another kind
untagKind := anonymous.AnonDevice
if kind == anonymous.AnonDevice {
untagKind = anonymous.AuthedDevice
}
if err := a.untagDevice(ctx, &Device{
Kind: untagKind,
IP: taggedDevice.IP,
UserAgent: taggedDevice.UserAgent,
}); err != nil {
return err
}
return nil return nil
} }
func (a *AnonDeviceService) Run(ctx context.Context) error {
ticker := time.NewTicker(2 * time.Hour)
for {
select {
case <-ticker.C:
err := a.serverLock.LockAndExecute(ctx, "cleanup old anon devices", time.Hour*10, func(context.Context) {
if err := a.anonStore.DeleteDevicesOlderThan(ctx, time.Now().Add(-keepFor)); err != nil {
a.log.Error("An error occurred while deleting old anon devices", "err", err)
}
})
if err != nil {
a.log.Error("Failed to lock and execute cleanup old anon devices", "error", err)
}
case <-ctx.Done():
return ctx.Err()
}
}
}

View File

@@ -2,7 +2,6 @@ package anonimpl
import ( import (
"context" "context"
"encoding/json"
"net/http" "net/http"
"testing" "testing"
"time" "time"
@@ -10,79 +9,30 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/services/anonymous" "github.com/grafana/grafana/pkg/services/anonymous"
"github.com/grafana/grafana/pkg/services/anonymous/anonimpl/anonstore"
"github.com/grafana/grafana/pkg/services/authn/authntest"
"github.com/grafana/grafana/pkg/services/org/orgtest"
"github.com/grafana/grafana/pkg/setting"
) )
func TestAnonDeviceKey(t *testing.T) {
testCases := []struct {
name string
session *Device
expected string
}{
{
name: "should hash correctly",
session: &Device{
Kind: anonymous.AnonDevice,
IP: "10.10.10.10",
UserAgent: "test",
},
expected: "anon-session:ad9f5c6bf504a9fa77c37a3a6658c0cd",
},
{
name: "should hash correctly with different ip",
session: &Device{
Kind: anonymous.AnonDevice,
IP: "10.10.10.1",
UserAgent: "test",
},
expected: "anon-session:580605320245e8289e0b301074a027c3",
},
{
name: "should hash correctly with different user agent",
session: &Device{
Kind: anonymous.AnonDevice,
IP: "10.10.10.1",
UserAgent: "test2",
},
expected: "anon-session:5fdd04b0bd04a9fa77c4243f8111258b",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
got, err := tc.session.Key()
require.NoError(t, err)
assert.Equal(t, tc.expected, got)
// ensure that the key is the same
got, err = tc.session.Key()
require.NoError(t, err)
assert.Equal(t, tc.expected, got)
})
}
}
func TestIntegrationDeviceService_tag(t *testing.T) { func TestIntegrationDeviceService_tag(t *testing.T) {
type tagReq struct { type tagReq struct {
httpReq *http.Request httpReq *http.Request
kind anonymous.DeviceKind kind anonymous.DeviceKind
} }
testCases := []struct { testCases := []struct {
name string name string
req []tagReq req []tagReq
expectedAnonCount int64 expectedAnonUICount int64
expectedAuthedCount int64 expectedKey string
expectedAnonUICount int64 expectedDevice *anonstore.Device
expectedAuthedUICount int64
expectedDevice *Device
}{ }{
{ {
name: "no requests", name: "no requests",
req: []tagReq{{httpReq: &http.Request{}, kind: anonymous.AnonDevice}}, req: []tagReq{{httpReq: &http.Request{}, kind: anonymous.AnonDeviceUI}},
expectedAnonCount: 0,
expectedAuthedCount: 0,
}, },
{ {
name: "missing info should not tag", name: "missing info should not tag",
@@ -91,28 +41,8 @@ func TestIntegrationDeviceService_tag(t *testing.T) {
"User-Agent": []string{"test"}, "User-Agent": []string{"test"},
}, },
}, },
kind: anonymous.AnonDevice, kind: anonymous.AnonDeviceUI,
}}, }},
expectedAnonCount: 0,
expectedAuthedCount: 0,
},
{
name: "should tag once",
req: []tagReq{{httpReq: &http.Request{
Header: http.Header{
"User-Agent": []string{"test"},
"X-Forwarded-For": []string{"10.30.30.1"},
},
},
kind: anonymous.AnonDevice,
},
},
expectedAnonCount: 1,
expectedAuthedCount: 0,
expectedDevice: &Device{
Kind: anonymous.AnonDevice,
IP: "10.30.30.1",
UserAgent: "test"},
}, },
{ {
name: "should tag device ID once", name: "should tag device ID once",
@@ -123,16 +53,14 @@ func TestIntegrationDeviceService_tag(t *testing.T) {
http.CanonicalHeaderKey(deviceIDHeader): []string{"32mdo31deeqwes"}, http.CanonicalHeaderKey(deviceIDHeader): []string{"32mdo31deeqwes"},
}, },
}, },
kind: anonymous.AnonDevice, kind: anonymous.AnonDeviceUI,
}, },
}, },
expectedAnonUICount: 1, expectedAnonUICount: 1,
expectedAuthedUICount: 0, expectedKey: "ui-anon-session:32mdo31deeqwes",
expectedAnonCount: 1, expectedDevice: &anonstore.Device{
expectedAuthedCount: 0, DeviceID: "32mdo31deeqwes",
expectedDevice: &Device{ ClientIP: "10.30.30.1",
Kind: anonymous.AnonDevice,
IP: "10.30.30.1",
UserAgent: "test"}, UserAgent: "test"},
}, },
{ {
@@ -144,7 +72,7 @@ func TestIntegrationDeviceService_tag(t *testing.T) {
"X-Forwarded-For": []string{"10.30.30.1"}, "X-Forwarded-For": []string{"10.30.30.1"},
}, },
}, },
kind: anonymous.AnonDevice, kind: anonymous.AnonDeviceUI,
}, {httpReq: &http.Request{ }, {httpReq: &http.Request{
Header: http.Header{ Header: http.Header{
"User-Agent": []string{"test"}, "User-Agent": []string{"test"},
@@ -152,61 +80,12 @@ func TestIntegrationDeviceService_tag(t *testing.T) {
"X-Forwarded-For": []string{"10.30.30.1"}, "X-Forwarded-For": []string{"10.30.30.1"},
}, },
}, },
kind: anonymous.AnonDevice, kind: anonymous.AnonDeviceUI,
}, },
}, },
expectedAnonCount: 1,
expectedAnonUICount: 1, expectedAnonUICount: 1,
expectedAuthedCount: 0,
}, { }, {
name: "authed request should untag anon", name: "tag 2 different requests",
req: []tagReq{{httpReq: &http.Request{
Header: http.Header{
"User-Agent": []string{"test"},
http.CanonicalHeaderKey(deviceIDHeader): []string{"32mdo31deeqwes"},
"X-Forwarded-For": []string{"10.30.30.1"},
},
},
kind: anonymous.AnonDevice,
}, {httpReq: &http.Request{
Header: http.Header{
"User-Agent": []string{"test"},
http.CanonicalHeaderKey(deviceIDHeader): []string{"32mdo31deeqwes"},
"X-Forwarded-For": []string{"10.30.30.1"},
},
},
kind: anonymous.AuthedDevice,
},
},
expectedAnonCount: 0,
expectedAuthedCount: 1,
expectedAuthedUICount: 1,
}, {
name: "anon request should untag authed",
req: []tagReq{{httpReq: &http.Request{
Header: http.Header{
"User-Agent": []string{"test"},
http.CanonicalHeaderKey(deviceIDHeader): []string{"32mdo31deeqwes"},
"X-Forwarded-For": []string{"10.30.30.1"},
},
},
kind: anonymous.AuthedDevice,
}, {httpReq: &http.Request{
Header: http.Header{
"User-Agent": []string{"test"},
http.CanonicalHeaderKey(deviceIDHeader): []string{"32mdo31deeqwes"},
"X-Forwarded-For": []string{"10.30.30.1"},
},
},
kind: anonymous.AnonDevice,
},
},
expectedAnonCount: 1,
expectedAnonUICount: 1,
expectedAuthedCount: 0,
},
{
name: "tag 4 different requests - 2 are UI",
req: []tagReq{{httpReq: &http.Request{ req: []tagReq{{httpReq: &http.Request{
Header: http.Header{ Header: http.Header{
http.CanonicalHeaderKey("User-Agent"): []string{"test"}, http.CanonicalHeaderKey("User-Agent"): []string{"test"},
@@ -214,106 +93,87 @@ func TestIntegrationDeviceService_tag(t *testing.T) {
http.CanonicalHeaderKey(deviceIDHeader): []string{"a"}, http.CanonicalHeaderKey(deviceIDHeader): []string{"a"},
}, },
}, },
kind: anonymous.AnonDevice, kind: anonymous.AnonDeviceUI,
}, {httpReq: &http.Request{
Header: http.Header{
"User-Agent": []string{"test"},
"X-Forwarded-For": []string{"10.30.30.2"},
},
},
kind: anonymous.AnonDevice,
}, {httpReq: &http.Request{ }, {httpReq: &http.Request{
Header: http.Header{ Header: http.Header{
"User-Agent": []string{"test"}, "User-Agent": []string{"test"},
"X-Forwarded-For": []string{"10.30.30.3"}, "X-Forwarded-For": []string{"10.30.30.2"},
http.CanonicalHeaderKey(deviceIDHeader): []string{"c"}, http.CanonicalHeaderKey(deviceIDHeader): []string{"b"},
}, },
}, },
kind: anonymous.AuthedDevice, kind: anonymous.AnonDeviceUI,
}, {httpReq: &http.Request{
Header: http.Header{
"User-Agent": []string{"test"},
"X-Forwarded-For": []string{"10.30.30.4"},
},
},
kind: anonymous.AuthedDevice,
}, },
}, },
expectedAnonCount: 2, expectedAnonUICount: 2,
expectedAuthedCount: 2,
expectedAnonUICount: 1,
expectedAuthedUICount: 1,
}, },
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
fakeStore := remotecache.NewFakeStore(t) store := db.InitTestDB(t)
anonDBStore := anonstore.ProvideAnonDBStore(store)
anonService := ProvideAnonymousDeviceService(fakeStore, &usagestats.UsageStatsMock{}) anonService := ProvideAnonymousDeviceService(&usagestats.UsageStatsMock{},
&authntest.FakeService{}, anonDBStore, setting.NewCfg(), orgtest.NewOrgServiceFake(), nil)
for _, req := range tc.req { for _, req := range tc.req {
err := anonService.TagDevice(context.Background(), req.httpReq, req.kind) err := anonService.TagDevice(context.Background(), req.httpReq, req.kind)
require.NoError(t, err) require.NoError(t, err)
} }
devices, err := anonDBStore.ListDevices(context.Background(), nil, nil)
require.NoError(t, err)
require.Len(t, devices, int(tc.expectedAnonUICount))
if tc.expectedDevice != nil {
device := devices[0]
assert.NotZero(t, device.ID)
assert.NotZero(t, device.CreatedAt)
assert.NotZero(t, device.UpdatedAt)
tc.expectedDevice.ID = device.ID
tc.expectedDevice.CreatedAt = device.CreatedAt
tc.expectedDevice.UpdatedAt = device.UpdatedAt
assert.Equal(t, tc.expectedDevice, devices[0])
}
stats, err := anonService.usageStatFn(context.Background()) stats, err := anonService.usageStatFn(context.Background())
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, tc.expectedAnonCount, stats["stats.anonymous.session.count"].(int64)) assert.Equal(t, tc.expectedAnonUICount, stats["stats.anonymous.device.ui.count"].(int64), stats)
assert.Equal(t, tc.expectedAuthedCount, stats["stats.users.device.count"].(int64))
assert.Equal(t, tc.expectedAnonUICount, stats["stats.anonymous.device.ui.count"].(int64))
assert.Equal(t, tc.expectedAuthedUICount, stats["stats.users.device.ui.count"].(int64))
if tc.expectedDevice != nil {
key, err := tc.expectedDevice.Key()
require.NoError(t, err)
k, err := fakeStore.Get(context.Background(), key)
require.NoError(t, err)
gotDevice := &Device{}
err = json.Unmarshal(k, gotDevice)
require.NoError(t, err)
assert.NotNil(t, gotDevice.LastSeen)
gotDevice.LastSeen = time.Time{}
assert.Equal(t, tc.expectedDevice, gotDevice)
}
}) })
} }
} }
// Ensure that the local cache prevents request from being tagged // Ensure that the local cache prevents request from being tagged
func TestIntegrationAnonDeviceService_localCacheSafety(t *testing.T) { func TestIntegrationAnonDeviceService_localCacheSafety(t *testing.T) {
fakeStore := remotecache.NewFakeStore(t) store := db.InitTestDB(t)
anonService := ProvideAnonymousDeviceService(fakeStore, &usagestats.UsageStatsMock{}) anonDBStore := anonstore.ProvideAnonDBStore(store)
anonService := ProvideAnonymousDeviceService(&usagestats.UsageStatsMock{},
&authntest.FakeService{}, anonDBStore, setting.NewCfg(), orgtest.NewOrgServiceFake(), nil)
req := &http.Request{ req := &http.Request{
Header: http.Header{ Header: http.Header{
"User-Agent": []string{"test"}, "User-Agent": []string{"test"},
"X-Forwarded-For": []string{"10.30.30.2"}, "X-Forwarded-For": []string{"10.30.30.2"},
http.CanonicalHeaderKey(deviceIDHeader): []string{"32mdo31deeqwes"},
}, },
} }
anonDevice := &Device{ anonDevice := &anonstore.Device{
Kind: anonymous.AnonDevice, DeviceID: "32mdo31deeqwes",
IP: "10.30.30.2", ClientIP: "10.30.30.2",
UserAgent: "test", UserAgent: "test",
LastSeen: time.Now().UTC(), UpdatedAt: time.Now().UTC(),
} }
key, err := anonDevice.Key() key := anonDevice.CacheKey()
require.NoError(t, err)
anonService.localCache.SetDefault(key, true) anonService.localCache.SetDefault(key, true)
err = anonService.TagDevice(context.Background(), req, anonymous.AnonDevice) err := anonService.TagDevice(context.Background(), req, anonymous.AnonDeviceUI)
require.NoError(t, err) require.NoError(t, err)
stats, err := anonService.usageStatFn(context.Background()) stats, err := anonService.usageStatFn(context.Background())
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, int64(0), stats["stats.anonymous.session.count"].(int64)) assert.Equal(t, int64(0), stats["stats.anonymous.device.ui.count"].(int64))
} }

View File

@@ -8,10 +8,7 @@ import (
type DeviceKind string type DeviceKind string
const ( const (
AnonDevice DeviceKind = "anon-session" AnonDeviceUI DeviceKind = "ui-anon-session"
AuthedDevice DeviceKind = "authed-session"
AnonDeviceUI DeviceKind = "ui-anon-session"
AuthedDeviceUI DeviceKind = "ui-authed-session"
) )
type Service interface { type Service interface {

View File

@@ -16,7 +16,6 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/login/social"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/anonymous"
"github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/apikey"
"github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn"
@@ -64,7 +63,6 @@ func ProvideService(
apikeyService apikey.Service, userService user.Service, apikeyService apikey.Service, userService user.Service,
jwtService auth.JWTVerifierService, jwtService auth.JWTVerifierService,
usageStats usagestats.Service, usageStats usagestats.Service,
anonDeviceService anonymous.Service,
userProtectionService login.UserProtectionService, userProtectionService login.UserProtectionService,
loginAttempts loginattempt.Service, quotaService quota.Service, loginAttempts loginattempt.Service, quotaService quota.Service,
authInfoService login.AuthInfoService, renderService rendering.Service, authInfoService login.AuthInfoService, renderService rendering.Service,
@@ -91,11 +89,7 @@ func ProvideService(
s.RegisterClient(clients.ProvideAPIKey(apikeyService, userService)) s.RegisterClient(clients.ProvideAPIKey(apikeyService, userService))
if cfg.LoginCookieName != "" { if cfg.LoginCookieName != "" {
s.RegisterClient(clients.ProvideSession(cfg, sessionService, features, anonDeviceService)) s.RegisterClient(clients.ProvideSession(cfg, sessionService, features))
}
if s.cfg.AnonymousEnabled {
s.RegisterClient(clients.ProvideAnonymous(cfg, orgService, anonDeviceService))
} }
var proxyClients []authn.ProxyClient var proxyClients []authn.ProxyClient

View File

@@ -3,13 +3,11 @@ package clients
import ( import (
"context" "context"
"errors" "errors"
"net/http"
"net/url" "net/url"
"time" "time"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/network" "github.com/grafana/grafana/pkg/infra/network"
"github.com/grafana/grafana/pkg/services/anonymous"
"github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
@@ -21,24 +19,20 @@ var _ authn.HookClient = new(Session)
var _ authn.ContextAwareClient = new(Session) var _ authn.ContextAwareClient = new(Session)
func ProvideSession(cfg *setting.Cfg, sessionService auth.UserTokenService, func ProvideSession(cfg *setting.Cfg, sessionService auth.UserTokenService,
features *featuremgmt.FeatureManager, anonDeviceService anonymous.Service) *Session { features *featuremgmt.FeatureManager) *Session {
return &Session{ return &Session{
cfg: cfg, cfg: cfg,
features: features, features: features,
sessionService: sessionService, sessionService: sessionService,
log: log.New(authn.ClientSession), log: log.New(authn.ClientSession),
anonDeviceService: anonDeviceService,
tagDevices: cfg.TagAuthedDevices,
} }
} }
type Session struct { type Session struct {
cfg *setting.Cfg cfg *setting.Cfg
features *featuremgmt.FeatureManager features *featuremgmt.FeatureManager
sessionService auth.UserTokenService sessionService auth.UserTokenService
log log.Logger log log.Logger
tagDevices bool
anonDeviceService anonymous.Service
} }
func (s *Session) Name() string { func (s *Session) Name() string {
@@ -67,29 +61,6 @@ func (s *Session) Authenticate(ctx context.Context, r *authn.Request) (*authn.Id
} }
} }
if s.tagDevices {
// Tag authed devices
httpReqCopy := &http.Request{}
if r.HTTPRequest != nil && r.HTTPRequest.Header != nil {
// avoid r.HTTPRequest.Clone(context.Background()) as we do not require a full clone
httpReqCopy.Header = r.HTTPRequest.Header.Clone()
httpReqCopy.RemoteAddr = r.HTTPRequest.RemoteAddr
}
go func() {
defer func() {
if err := recover(); err != nil {
s.log.Warn("Tag anon session panic", "err", err)
}
}()
newCtx, cancel := context.WithTimeout(context.Background(), timeoutTag)
defer cancel()
if err := s.anonDeviceService.TagDevice(newCtx, httpReqCopy, anonymous.AuthedDevice); err != nil {
s.log.Warn("Failed to tag anonymous session", "error", err)
}
}()
}
return &authn.Identity{ return &authn.Identity{
ID: authn.NamespacedID(authn.NamespaceUser, token.UserId), ID: authn.NamespacedID(authn.NamespaceUser, token.UserId),
SessionToken: token, SessionToken: token,

View File

@@ -11,7 +11,6 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/models/usertoken" "github.com/grafana/grafana/pkg/models/usertoken"
"github.com/grafana/grafana/pkg/services/anonymous/anontest"
"github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/auth/authtest" "github.com/grafana/grafana/pkg/services/auth/authtest"
"github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn"
@@ -30,7 +29,7 @@ func TestSession_Test(t *testing.T) {
cfg := setting.NewCfg() cfg := setting.NewCfg()
cfg.LoginCookieName = "" cfg.LoginCookieName = ""
cfg.LoginMaxLifetime = 20 * time.Second cfg.LoginMaxLifetime = 20 * time.Second
s := ProvideSession(cfg, &authtest.FakeUserAuthTokenService{}, featuremgmt.WithFeatures(), &anontest.FakeAnonymousSessionService{}) s := ProvideSession(cfg, &authtest.FakeUserAuthTokenService{}, featuremgmt.WithFeatures())
disabled := s.Test(context.Background(), &authn.Request{HTTPRequest: validHTTPReq}) disabled := s.Test(context.Background(), &authn.Request{HTTPRequest: validHTTPReq})
assert.False(t, disabled) assert.False(t, disabled)
@@ -146,7 +145,7 @@ func TestSession_Authenticate(t *testing.T) {
cfg.LoginCookieName = cookieName cfg.LoginCookieName = cookieName
cfg.TokenRotationIntervalMinutes = 10 cfg.TokenRotationIntervalMinutes = 10
cfg.LoginMaxLifetime = 20 * time.Second cfg.LoginMaxLifetime = 20 * time.Second
s := ProvideSession(cfg, tt.fields.sessionService, tt.fields.features, &anontest.FakeAnonymousSessionService{}) s := ProvideSession(cfg, tt.fields.sessionService, tt.fields.features)
got, err := s.Authenticate(context.Background(), tt.args.r) got, err := s.Authenticate(context.Background(), tt.args.r)
require.True(t, (err != nil) == tt.wantErr, err) require.True(t, (err != nil) == tt.wantErr, err)
@@ -186,7 +185,7 @@ func TestSession_Hook(t *testing.T) {
token.UnhashedToken = "new-token" token.UnhashedToken = "new-token"
return true, token, nil return true, token, nil
}, },
}, featuremgmt.WithFeatures(), &anontest.FakeAnonymousSessionService{}) }, featuremgmt.WithFeatures())
sampleID := &authn.Identity{ sampleID := &authn.Identity{
SessionToken: &auth.UserToken{ SessionToken: &auth.UserToken{
@@ -220,7 +219,7 @@ func TestSession_Hook(t *testing.T) {
}) })
t.Run("should not rotate token with feature flag", func(t *testing.T) { t.Run("should not rotate token with feature flag", func(t *testing.T) {
s := ProvideSession(setting.NewCfg(), nil, featuremgmt.WithFeatures(featuremgmt.FlagClientTokenRotation), &anontest.FakeAnonymousSessionService{}) s := ProvideSession(setting.NewCfg(), nil, featuremgmt.WithFeatures(featuremgmt.FlagClientTokenRotation))
req := &authn.Request{} req := &authn.Request{}
identity := &authn.Identity{} identity := &authn.Identity{}

View File

@@ -0,0 +1,25 @@
package anonservice
import "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
func AddMigration(mg *migrator.Migrator) {
var anonV1 = migrator.Table{
Name: "anon_device",
Columns: []*migrator.Column{
{Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "client_ip", Type: migrator.DB_NVarchar, Length: 255, Nullable: false},
{Name: "created_at", Type: migrator.DB_DateTime, Nullable: false},
{Name: "device_id", Type: migrator.DB_NVarchar, Length: 127, Nullable: false},
{Name: "updated_at", Type: migrator.DB_DateTime, Nullable: false},
{Name: "user_agent", Type: migrator.DB_NVarchar, Length: 255, Nullable: false},
},
Indices: []*migrator.Index{
{Cols: []string{"device_id"}, Type: migrator.UniqueIndex},
{Cols: []string{"updated_at"}, Type: migrator.IndexType},
},
}
mg.AddMigration("create anon_device table", migrator.NewAddTableMigration(anonV1))
mg.AddMigration("add unique index anon_device.device_id", migrator.NewAddIndexMigration(anonV1, anonV1.Indices[0]))
mg.AddMigration("add index anon_device.updated_at", migrator.NewAddIndexMigration(anonV1, anonV1.Indices[1]))
}

View File

@@ -3,6 +3,7 @@ package migrations
import ( import (
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol" "github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/anonservice"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/oauthserver" "github.com/grafana/grafana/pkg/services/sqlstore/migrations/oauthserver"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/ualert" "github.com/grafana/grafana/pkg/services/sqlstore/migrations/ualert"
. "github.com/grafana/grafana/pkg/services/sqlstore/migrator" . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
@@ -96,6 +97,8 @@ func (*OSSMigrations) AddMigration(mg *Migrator) {
oauthserver.AddMigration(mg) oauthserver.AddMigration(mg)
} }
} }
anonservice.AddMigration(mg)
} }
func addStarMigrations(mg *Migrator) { func addStarMigrations(mg *Migrator) {

View File

@@ -287,8 +287,6 @@ type Cfg struct {
// Not documented & not supported // Not documented & not supported
// stand in until a more complete solution is implemented // stand in until a more complete solution is implemented
AuthConfigUIAdminAccess bool AuthConfigUIAdminAccess bool
// TO REMOVE: Not documented & not supported. Remove in 10.3
TagAuthedDevices bool
// AWS Plugin Auth // AWS Plugin Auth
AWSAllowedAuthProviders []string AWSAllowedAuthProviders []string
@@ -1551,7 +1549,6 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) {
// Do not use // Do not use
cfg.AuthConfigUIAdminAccess = auth.Key("config_ui_admin_access").MustBool(false) cfg.AuthConfigUIAdminAccess = auth.Key("config_ui_admin_access").MustBool(false)
cfg.TagAuthedDevices = auth.Key("tag_authed_devices").MustBool(true)
cfg.DisableLoginForm = auth.Key("disable_login_form").MustBool(false) cfg.DisableLoginForm = auth.Key("disable_login_form").MustBool(false)
DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false) DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false)