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
+1 -7
View File
@@ -16,7 +16,6 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/login/social"
"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/auth"
"github.com/grafana/grafana/pkg/services/authn"
@@ -64,7 +63,6 @@ func ProvideService(
apikeyService apikey.Service, userService user.Service,
jwtService auth.JWTVerifierService,
usageStats usagestats.Service,
anonDeviceService anonymous.Service,
userProtectionService login.UserProtectionService,
loginAttempts loginattempt.Service, quotaService quota.Service,
authInfoService login.AuthInfoService, renderService rendering.Service,
@@ -91,11 +89,7 @@ func ProvideService(
s.RegisterClient(clients.ProvideAPIKey(apikeyService, userService))
if cfg.LoginCookieName != "" {
s.RegisterClient(clients.ProvideSession(cfg, sessionService, features, anonDeviceService))
}
if s.cfg.AnonymousEnabled {
s.RegisterClient(clients.ProvideAnonymous(cfg, orgService, anonDeviceService))
s.RegisterClient(clients.ProvideSession(cfg, sessionService, features))
}
var proxyClients []authn.ProxyClient
-96
View File
@@ -1,96 +0,0 @@
package clients
import (
"context"
"net/http"
"strings"
"time"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/anonymous"
"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
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 {
cfg *setting.Cfg
log log.Logger
orgService org.Service
anonDeviceService anonymous.Service
}
func (a *Anonymous) Name() string {
return authn.ClientAnonymous
}
func (a *Anonymous) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) {
o, err := a.orgService.GetByName(ctx, &org.GetOrgByNameQuery{Name: a.cfg.AnonymousOrgName})
if err != nil {
a.log.FromContext(ctx).Error("Failed to find organization", "name", a.cfg.AnonymousOrgName, "error", err)
return nil, err
}
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 {
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.AnonDevice); err != nil {
a.log.Warn("Failed to tag anonymous session", "error", err)
}
}()
return &authn.Identity{
IsAnonymous: true,
OrgID: o.ID,
OrgName: o.Name,
OrgRoles: map[int64]org.RoleType{o.ID: org.RoleType(a.cfg.AnonymousOrgRole)},
ClientParams: authn.ClientParams{SyncPermissions: true},
}, nil
}
func (a *Anonymous) Test(ctx context.Context, r *authn.Request) bool {
// If anonymous client is register it can always be used for authentication
return true
}
func (a *Anonymous) Priority() uint {
return 100
}
func (a *Anonymous) UsageStatFn(ctx context.Context) (map[string]any, error) {
m := map[string]any{}
// Add stats about anonymous auth
m["stats.anonymous.customized_role.count"] = 0
if !strings.EqualFold(a.cfg.AnonymousOrgRole, "Viewer") {
m["stats.anonymous.customized_role.count"] = 1
}
return m, nil
}
@@ -1,69 +0,0 @@
package clients
import (
"context"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/anonymous/anontest"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgtest"
"github.com/grafana/grafana/pkg/setting"
)
func TestAnonymous_Authenticate(t *testing.T) {
type TestCase struct {
desc string
org *org.Org
cfg *setting.Cfg
err error
}
tests := []TestCase{
{
desc: "should success with valid org configured",
org: &org.Org{ID: 1, Name: "some org"},
cfg: &setting.Cfg{
AnonymousOrgName: "some org",
AnonymousOrgRole: "Viewer",
},
},
{
desc: "should return error if any error occurs during org lookup",
err: fmt.Errorf("some error"),
cfg: &setting.Cfg{
AnonymousOrgName: "some org",
AnonymousOrgRole: "Viewer",
},
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
c := Anonymous{
cfg: tt.cfg,
log: log.NewNopLogger(),
orgService: &orgtest.FakeOrgService{ExpectedOrg: tt.org, ExpectedError: tt.err},
anonDeviceService: &anontest.FakeAnonymousSessionService{},
}
identity, err := c.Authenticate(context.Background(), &authn.Request{})
if err != nil {
require.Error(t, err)
require.Nil(t, identity)
} else {
require.Nil(t, err)
assert.Equal(t, true, identity.ID == "")
assert.Equal(t, tt.org.ID, identity.OrgID)
assert.Equal(t, tt.org.Name, identity.OrgName)
assert.Equal(t, tt.cfg.AnonymousOrgRole, string(identity.Role()))
}
})
}
}
+9 -38
View File
@@ -3,13 +3,11 @@ package clients
import (
"context"
"errors"
"net/http"
"net/url"
"time"
"github.com/grafana/grafana/pkg/infra/log"
"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/authn"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@@ -21,24 +19,20 @@ var _ authn.HookClient = new(Session)
var _ authn.ContextAwareClient = new(Session)
func ProvideSession(cfg *setting.Cfg, sessionService auth.UserTokenService,
features *featuremgmt.FeatureManager, anonDeviceService anonymous.Service) *Session {
features *featuremgmt.FeatureManager) *Session {
return &Session{
cfg: cfg,
features: features,
sessionService: sessionService,
log: log.New(authn.ClientSession),
anonDeviceService: anonDeviceService,
tagDevices: cfg.TagAuthedDevices,
cfg: cfg,
features: features,
sessionService: sessionService,
log: log.New(authn.ClientSession),
}
}
type Session struct {
cfg *setting.Cfg
features *featuremgmt.FeatureManager
sessionService auth.UserTokenService
log log.Logger
tagDevices bool
anonDeviceService anonymous.Service
cfg *setting.Cfg
features *featuremgmt.FeatureManager
sessionService auth.UserTokenService
log log.Logger
}
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{
ID: authn.NamespacedID(authn.NamespaceUser, token.UserId),
SessionToken: token,
+4 -5
View File
@@ -11,7 +11,6 @@ import (
"github.com/stretchr/testify/require"
"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/authtest"
"github.com/grafana/grafana/pkg/services/authn"
@@ -30,7 +29,7 @@ func TestSession_Test(t *testing.T) {
cfg := setting.NewCfg()
cfg.LoginCookieName = ""
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})
assert.False(t, disabled)
@@ -146,7 +145,7 @@ func TestSession_Authenticate(t *testing.T) {
cfg.LoginCookieName = cookieName
cfg.TokenRotationIntervalMinutes = 10
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)
require.True(t, (err != nil) == tt.wantErr, err)
@@ -186,7 +185,7 @@ func TestSession_Hook(t *testing.T) {
token.UnhashedToken = "new-token"
return true, token, nil
},
}, featuremgmt.WithFeatures(), &anontest.FakeAnonymousSessionService{})
}, featuremgmt.WithFeatures())
sampleID := &authn.Identity{
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) {
s := ProvideSession(setting.NewCfg(), nil, featuremgmt.WithFeatures(featuremgmt.FlagClientTokenRotation), &anontest.FakeAnonymousSessionService{})
s := ProvideSession(setting.NewCfg(), nil, featuremgmt.WithFeatures(featuremgmt.FlagClientTokenRotation))
req := &authn.Request{}
identity := &authn.Identity{}