Auth: Attach external session info to Grafana session (#93849)

* initial from poc changes

* wip

* Remove public external session service

* Update swagger

* Fix merge

* Cleanup

* Add backgroud service for cleanup

* Add auth_module to user_external_session

* Add tests for token revocation functions

* Add secret migration capabilities for user_external_session fields

* Cleanup, refactor to address feedback

* Fix test
This commit is contained in:
Misi 2024-10-08 11:03:29 +02:00 committed by GitHub
parent 9eea0e99fc
commit bd7850853e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 1419 additions and 161 deletions

View File

@ -255,7 +255,7 @@ func (hs *HTTPServer) loginUserWithUser(user *user.User, c *contextmodel.ReqCont
hs.log.Debug("Got IP address from client address", "addr", addr, "ip", ip)
ctx := context.WithValue(c.Req.Context(), loginservice.RequestURIKey{}, c.Req.RequestURI)
userToken, err := hs.AuthTokenService.CreateToken(ctx, user, ip, c.Req.UserAgent())
userToken, err := hs.AuthTokenService.CreateToken(ctx, &auth.CreateTokenCommand{User: user, ClientIP: ip, UserAgent: c.Req.UserAgent()})
if err != nil {
return fmt.Errorf("%v: %w", "failed to create auth token", err)
}

View File

@ -22,19 +22,20 @@ func (e *TokenRevokedError) Unwrap() error { return ErrInvalidSessionToken }
// UserToken represents a user token
type UserToken struct {
Id int64
UserId int64
AuthToken string
PrevAuthToken string
UserAgent string
ClientIp string
AuthTokenSeen bool
SeenAt int64
RotatedAt int64
CreatedAt int64
UpdatedAt int64
RevokedAt int64
UnhashedToken string
Id int64
UserId int64
ExternalSessionId int64
AuthToken string
PrevAuthToken string
UserAgent string
ClientIp string
AuthTokenSeen bool
SeenAt int64
RotatedAt int64
CreatedAt int64
UpdatedAt int64
RevokedAt int64
UnhashedToken string
}
const UrgentRotateTime = 1 * time.Minute

View File

@ -20,8 +20,9 @@ const (
// Typed errors
var (
ErrUserTokenNotFound = errors.New("user token not found")
ErrInvalidSessionToken = usertoken.ErrInvalidSessionToken
ErrUserTokenNotFound = errors.New("user token not found")
ErrInvalidSessionToken = usertoken.ErrInvalidSessionToken
ErrExternalSessionNotFound = errors.New("external session not found")
)
type (
@ -65,10 +66,21 @@ type RotateCommand struct {
UserAgent string
}
type CreateTokenCommand struct {
User *user.User
ClientIP net.IP
UserAgent string
ExternalSession *ExternalSession
}
// UserTokenService are used for generating and validating user tokens
type UserTokenService interface {
CreateToken(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*UserToken, error)
CreateToken(ctx context.Context, cmd *CreateTokenCommand) (*UserToken, error)
LookupToken(ctx context.Context, unhashedToken string) (*UserToken, error)
GetTokenByExternalSessionID(ctx context.Context, externalSessionID int64) (*UserToken, error)
GetExternalSession(ctx context.Context, extSessionID int64) (*ExternalSession, error)
FindExternalSessions(ctx context.Context, query *ListExternalSessionQuery) ([]*ExternalSession, error)
// RotateToken will always rotate a valid token
RotateToken(ctx context.Context, cmd RotateCommand) (*UserToken, error)
RevokeToken(ctx context.Context, token *UserToken, soft bool) error

View File

@ -14,10 +14,11 @@ import (
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/models/usertoken"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@ -28,10 +29,13 @@ var (
errUserIDInvalid = errors.New("invalid user ID")
)
var _ auth.UserTokenService = (*UserAuthTokenService)(nil)
func ProvideUserAuthTokenService(sqlStore db.DB,
serverLockService *serverlock.ServerLockService,
quotaService quota.Service,
cfg *setting.Cfg) (*UserAuthTokenService, error) {
quotaService quota.Service, secretService secrets.Service,
cfg *setting.Cfg, tracer tracing.Tracer,
) (*UserAuthTokenService, error) {
s := &UserAuthTokenService{
sqlStore: sqlStore,
serverLockService: serverLockService,
@ -39,6 +43,7 @@ func ProvideUserAuthTokenService(sqlStore db.DB,
log: log.New("auth"),
singleflight: new(singleflight.Group),
}
s.externalSessionStore = provideExternalSessionStore(sqlStore, secretService, tracer)
defaultLimits, err := readQuotaConfig(cfg)
if err != nil {
@ -57,31 +62,32 @@ func ProvideUserAuthTokenService(sqlStore db.DB,
}
type UserAuthTokenService struct {
sqlStore db.DB
serverLockService *serverlock.ServerLockService
cfg *setting.Cfg
log log.Logger
singleflight *singleflight.Group
sqlStore db.DB
serverLockService *serverlock.ServerLockService
cfg *setting.Cfg
log log.Logger
externalSessionStore auth.ExternalSessionStore
singleflight *singleflight.Group
}
func (s *UserAuthTokenService) CreateToken(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*auth.UserToken, error) {
func (s *UserAuthTokenService) CreateToken(ctx context.Context, cmd *auth.CreateTokenCommand) (*auth.UserToken, error) {
token, hashedToken, err := generateAndHashToken(s.cfg.SecretKey)
if err != nil {
return nil, err
}
now := getTime().Unix()
clientIPStr := clientIP.String()
if len(clientIP) == 0 {
clientIPStr := cmd.ClientIP.String()
if len(cmd.ClientIP) == 0 {
clientIPStr = ""
}
userAuthToken := userAuthToken{
UserId: user.ID,
UserId: cmd.User.ID,
AuthToken: hashedToken,
PrevAuthToken: hashedToken,
ClientIp: clientIPStr,
UserAgent: userAgent,
UserAgent: cmd.UserAgent,
RotatedAt: now,
CreatedAt: now,
UpdatedAt: now,
@ -90,11 +96,21 @@ func (s *UserAuthTokenService) CreateToken(ctx context.Context, user *user.User,
AuthTokenSeen: false,
}
err = s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
_, err = dbSession.Insert(&userAuthToken)
return err
})
err = s.sqlStore.InTransaction(ctx, func(ctx context.Context) error {
if cmd.ExternalSession != nil {
inErr := s.externalSessionStore.Create(ctx, cmd.ExternalSession)
if inErr != nil {
return inErr
}
userAuthToken.ExternalSessionId = cmd.ExternalSession.ID
}
inErr := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
_, err := dbSession.Insert(&userAuthToken)
return err
})
return inErr
})
if err != nil {
return nil, err
}
@ -164,7 +180,6 @@ func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken st
return err
})
if err != nil {
return nil, err
}
@ -190,7 +205,6 @@ func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken st
return err
})
if err != nil {
return nil, err
}
@ -210,6 +224,38 @@ func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken st
return &userToken, err
}
func (s *UserAuthTokenService) GetTokenByExternalSessionID(ctx context.Context, externalSessionID int64) (*auth.UserToken, error) {
var token userAuthToken
err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
exists, err := dbSession.Where("external_session_id = ?", externalSessionID).Get(&token)
if err != nil {
return err
}
if !exists {
return auth.ErrUserTokenNotFound
}
return nil
})
if err != nil {
return nil, err
}
var userToken auth.UserToken
err = token.toUserToken(&userToken)
return &userToken, err
}
func (s *UserAuthTokenService) GetExternalSession(ctx context.Context, extSessionID int64) (*auth.ExternalSession, error) {
return s.externalSessionStore.Get(ctx, extSessionID)
}
func (s *UserAuthTokenService) FindExternalSessions(ctx context.Context, query *auth.ListExternalSessionQuery) ([]*auth.ExternalSession, error) {
return s.externalSessionStore.List(ctx, query)
}
func (s *UserAuthTokenService) RotateToken(ctx context.Context, cmd auth.RotateCommand) (*auth.UserToken, error) {
if cmd.UnHashedToken == "" {
return nil, auth.ErrInvalidSessionToken
@ -277,7 +323,6 @@ func (s *UserAuthTokenService) rotateToken(ctx context.Context, token *auth.User
affected, err = res.RowsAffected()
return err
})
if err != nil {
return nil, err
}
@ -305,6 +350,8 @@ func (s *UserAuthTokenService) RevokeToken(ctx context.Context, token *auth.User
return err
}
ctxLogger := s.log.FromContext(ctx)
var rowsAffected int64
if soft {
@ -324,7 +371,13 @@ func (s *UserAuthTokenService) RevokeToken(ctx context.Context, token *auth.User
return err
}
ctxLogger := s.log.FromContext(ctx)
if model.ExternalSessionId != 0 {
err = s.externalSessionStore.Delete(ctx, model.ExternalSessionId)
if err != nil {
// Intentionally not returning error here, as the token has been revoked -> the backround job will clean up orphaned external sessions
ctxLogger.Warn("Failed to delete external session", "externalSessionID", model.ExternalSessionId, "err", err)
}
}
if rowsAffected == 0 {
ctxLogger.Debug("User auth token not found/revoked", "tokenID", model.Id, "userID", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent)
@ -337,51 +390,75 @@ func (s *UserAuthTokenService) RevokeToken(ctx context.Context, token *auth.User
}
func (s *UserAuthTokenService) RevokeAllUserTokens(ctx context.Context, userId int64) error {
return s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
sql := `DELETE from user_auth_token WHERE user_id = ?`
res, err := dbSession.Exec(sql, userId)
return s.sqlStore.InTransaction(ctx, func(ctx context.Context) error {
ctxLogger := s.log.FromContext(ctx)
err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
sql := `DELETE from user_auth_token WHERE user_id = ?`
res, err := dbSession.Exec(sql, userId)
if err != nil {
return err
}
affected, err := res.RowsAffected()
if err != nil {
return err
}
ctxLogger.Debug("All user tokens for user revoked", "userID", userId, "count", affected)
return nil
})
if err != nil {
return err
}
affected, err := res.RowsAffected()
err = s.externalSessionStore.DeleteExternalSessionsByUserID(ctx, userId)
if err != nil {
return err
// Intentionally not returning error here, as the token has been revoked -> the backround job will clean up orphaned external sessions
ctxLogger.Warn("Failed to delete external sessions for user", "userID", userId, "err", err)
}
s.log.FromContext(ctx).Debug("All user tokens for user revoked", "userID", userId, "count", affected)
return err
return nil
})
}
func (s *UserAuthTokenService) BatchRevokeAllUserTokens(ctx context.Context, userIds []int64) error {
return s.sqlStore.WithTransactionalDbSession(ctx, func(dbSession *db.Session) error {
return s.sqlStore.InTransaction(ctx, func(ctx context.Context) error {
ctxLogger := s.log.FromContext(ctx)
if len(userIds) == 0 {
return nil
}
user_id_params := strings.Repeat(",?", len(userIds)-1)
sql := "DELETE from user_auth_token WHERE user_id IN (?" + user_id_params + ")"
userIdParams := strings.Repeat(",?", len(userIds)-1)
sql := "DELETE from user_auth_token WHERE user_id IN (?" + userIdParams + ")"
params := []any{sql}
for _, v := range userIds {
params = append(params, v)
}
res, err := dbSession.Exec(params...)
var affected int64
err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
res, inErr := dbSession.Exec(params...)
if inErr != nil {
return inErr
}
affected, inErr = res.RowsAffected()
return inErr
})
if err != nil {
return err
}
affected, err := res.RowsAffected()
err = s.externalSessionStore.BatchDeleteExternalSessionsByUserIDs(ctx, userIds)
if err != nil {
return err
ctxLogger.Warn("Failed to delete external sessions for users", "users", userIds, "err", err)
}
s.log.FromContext(ctx).Debug("All user tokens for given users revoked", "usersCount", len(userIds), "count", affected)
ctxLogger.Debug("All user tokens for given users revoked", "usersCount", len(userIds), "count", affected)
return err
return nil
})
}

View File

@ -3,20 +3,25 @@ package authimpl
import (
"context"
"encoding/json"
"errors"
"net"
"reflect"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"golang.org/x/sync/singleflight"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/auth/authtest"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tests/testsuite"
@ -36,8 +41,11 @@ func TestIntegrationUserAuthToken(t *testing.T) {
t.Run("When creating token", func(t *testing.T) {
createToken := func() *auth.UserToken {
userToken, err := ctx.tokenService.CreateToken(context.Background(), usr,
net.ParseIP("192.168.10.11"), "some user agent")
userToken, err := ctx.tokenService.CreateToken(context.Background(), &auth.CreateTokenCommand{
User: usr,
ClientIP: net.ParseIP("192.168.10.11"),
UserAgent: "some user agent",
})
require.Nil(t, err)
require.NotNil(t, userToken)
require.False(t, userToken.AuthTokenSeen)
@ -109,8 +117,11 @@ func TestIntegrationUserAuthToken(t *testing.T) {
userToken = createToken()
t.Run("When creating an additional token", func(t *testing.T) {
userToken2, err := ctx.tokenService.CreateToken(context.Background(), usr,
net.ParseIP("192.168.10.11"), "some user agent")
userToken2, err := ctx.tokenService.CreateToken(context.Background(), &auth.CreateTokenCommand{
User: usr,
ClientIP: net.ParseIP("192.168.10.11"),
UserAgent: "some user agent",
})
require.Nil(t, err)
require.NotNil(t, userToken2)
@ -156,8 +167,11 @@ func TestIntegrationUserAuthToken(t *testing.T) {
for i := 0; i < 3; i++ {
userId := usr.ID + int64(i+1)
userIds = append(userIds, userId)
_, err := ctx.tokenService.CreateToken(context.Background(), usr,
net.ParseIP("192.168.10.11"), "some user agent")
_, err := ctx.tokenService.CreateToken(context.Background(), &auth.CreateTokenCommand{
User: usr,
ClientIP: net.ParseIP("192.168.10.11"),
UserAgent: "some user agent",
})
require.Nil(t, err)
}
@ -173,10 +187,89 @@ func TestIntegrationUserAuthToken(t *testing.T) {
})
})
t.Run("When creating token with external session", func(t *testing.T) {
createToken := func() *auth.UserToken {
userToken, err := ctx.tokenService.CreateToken(context.Background(), &auth.CreateTokenCommand{
User: usr,
ClientIP: net.ParseIP("192.168.10.11"),
UserAgent: "some user agent",
ExternalSession: &auth.ExternalSession{UserID: usr.ID, AuthModule: "test", UserAuthID: 1},
})
require.Nil(t, err)
require.NotNil(t, userToken)
require.False(t, userToken.AuthTokenSeen)
return userToken
}
userToken := createToken()
t.Run("soft revoking existing token should remove the associated external session", func(t *testing.T) {
err := ctx.tokenService.RevokeToken(context.Background(), userToken, true)
require.Nil(t, err)
model, err := ctx.getAuthTokenByID(userToken.Id)
require.Nil(t, err)
require.NotNil(t, model)
require.Greater(t, model.RevokedAt, int64(0))
extSess, err := ctx.getExternalSessionByID(userToken.ExternalSessionId)
require.Nil(t, err)
require.Nil(t, extSess)
})
t.Run("revoking existing token should also remove the associated external session", func(t *testing.T) {
err := ctx.tokenService.RevokeToken(context.Background(), userToken, false)
require.Nil(t, err)
model, err := ctx.getAuthTokenByID(userToken.Id)
require.Nil(t, err)
require.Nil(t, model)
extSess, err := ctx.getExternalSessionByID(userToken.ExternalSessionId)
require.Nil(t, err)
require.Nil(t, extSess)
})
t.Run("When revoking users tokens in a batch", func(t *testing.T) {
t.Run("Can revoke all users tokens and associated external sessions", func(t *testing.T) {
userIds := []int64{}
extSessionIds := []int64{}
for i := 0; i < 3; i++ {
userId := usr.ID + int64(i+1)
userIds = append(userIds, userId)
token, err := ctx.tokenService.CreateToken(context.Background(), &auth.CreateTokenCommand{
User: usr,
ClientIP: net.ParseIP("192.168.10.11"),
UserAgent: "some user agent",
ExternalSession: &auth.ExternalSession{UserID: userId, AuthModule: "test", UserAuthID: 1},
})
require.Nil(t, err)
extSessionIds = append(extSessionIds, token.ExternalSessionId)
}
err := ctx.tokenService.BatchRevokeAllUserTokens(context.Background(), userIds)
require.Nil(t, err)
for i := 0; i < len(userIds); i++ {
tokens, err := ctx.tokenService.GetUserTokens(context.Background(), userIds[i])
require.Nil(t, err)
require.Equal(t, 0, len(tokens))
extSess, err := ctx.getExternalSessionByID(extSessionIds[i])
require.Nil(t, err)
require.Nil(t, extSess)
}
})
})
})
t.Run("expires correctly", func(t *testing.T) {
ctx := createTestContext(t)
userToken, err := ctx.tokenService.CreateToken(context.Background(), usr,
net.ParseIP("192.168.10.11"), "some user agent")
userToken, err := ctx.tokenService.CreateToken(context.Background(), &auth.CreateTokenCommand{
User: usr,
ClientIP: net.ParseIP("192.168.10.11"),
UserAgent: "some user agent",
})
require.Nil(t, err)
userToken, err = ctx.tokenService.LookupToken(context.Background(), userToken.UnhashedToken)
@ -262,7 +355,11 @@ func TestIntegrationUserAuthToken(t *testing.T) {
t.Run("can properly rotate tokens", func(t *testing.T) {
getTime = func() time.Time { return now }
ctx := createTestContext(t)
userToken, err := ctx.tokenService.CreateToken(context.Background(), usr, net.ParseIP("192.168.10.11"), "some user agent")
userToken, err := ctx.tokenService.CreateToken(context.Background(), &auth.CreateTokenCommand{
User: usr,
ClientIP: net.ParseIP("192.168.10.11"),
UserAgent: "some user agent",
})
require.Nil(t, err)
prevToken := userToken.AuthToken
@ -335,8 +432,11 @@ func TestIntegrationUserAuthToken(t *testing.T) {
t.Run("keeps prev token valid for 1 minute after it is confirmed", func(t *testing.T) {
getTime = func() time.Time { return now }
userToken, err := ctx.tokenService.CreateToken(context.Background(), usr,
net.ParseIP("192.168.10.11"), "some user agent")
userToken, err := ctx.tokenService.CreateToken(context.Background(), &auth.CreateTokenCommand{
User: usr,
ClientIP: net.ParseIP("192.168.10.11"),
UserAgent: "some user agent",
})
require.Nil(t, err)
require.NotNil(t, userToken)
@ -368,8 +468,11 @@ func TestIntegrationUserAuthToken(t *testing.T) {
})
t.Run("will not mark token unseen when prev and current are the same", func(t *testing.T) {
userToken, err := ctx.tokenService.CreateToken(context.Background(), usr,
net.ParseIP("192.168.10.11"), "some user agent")
userToken, err := ctx.tokenService.CreateToken(context.Background(), &auth.CreateTokenCommand{
User: usr,
ClientIP: net.ParseIP("192.168.10.11"),
UserAgent: "some user agent",
})
require.Nil(t, err)
require.NotNil(t, userToken)
@ -389,7 +492,11 @@ func TestIntegrationUserAuthToken(t *testing.T) {
t.Run("RotateToken", func(t *testing.T) {
var prev string
token, err := ctx.tokenService.CreateToken(context.Background(), usr, nil, "")
token, err := ctx.tokenService.CreateToken(context.Background(), &auth.CreateTokenCommand{
User: usr,
ClientIP: nil,
UserAgent: "",
})
require.NoError(t, err)
t.Run("should rotate token when called with current auth token", func(t *testing.T) {
prev = token.UnhashedToken
@ -412,7 +519,11 @@ func TestIntegrationUserAuthToken(t *testing.T) {
})
t.Run("should return error when token is revoked", func(t *testing.T) {
revokedToken, err := ctx.tokenService.CreateToken(context.Background(), usr, nil, "")
revokedToken, err := ctx.tokenService.CreateToken(context.Background(), &auth.CreateTokenCommand{
User: usr,
ClientIP: nil,
UserAgent: "",
})
require.NoError(t, err)
// mark token as revoked
err = ctx.sqlstore.WithDbSession(context.Background(), func(sess *db.Session) error {
@ -426,7 +537,11 @@ func TestIntegrationUserAuthToken(t *testing.T) {
})
t.Run("should return error when token has expired", func(t *testing.T) {
expiredToken, err := ctx.tokenService.CreateToken(context.Background(), usr, nil, "")
expiredToken, err := ctx.tokenService.CreateToken(context.Background(), &auth.CreateTokenCommand{
User: usr,
ClientIP: nil,
UserAgent: "",
})
require.NoError(t, err)
// mark token as expired
err = ctx.sqlstore.WithDbSession(context.Background(), func(sess *db.Session) error {
@ -441,10 +556,18 @@ func TestIntegrationUserAuthToken(t *testing.T) {
t.Run("should only delete revoked tokens that are outside on specified window", func(t *testing.T) {
usr := &user.User{ID: 100}
token1, err := ctx.tokenService.CreateToken(context.Background(), usr, nil, "")
token1, err := ctx.tokenService.CreateToken(context.Background(), &auth.CreateTokenCommand{
User: usr,
ClientIP: nil,
UserAgent: "",
})
require.NoError(t, err)
token2, err := ctx.tokenService.CreateToken(context.Background(), usr, nil, "")
token2, err := ctx.tokenService.CreateToken(context.Background(), &auth.CreateTokenCommand{
User: usr,
ClientIP: nil,
UserAgent: "",
})
require.NoError(t, err)
getTime = func() time.Time {
@ -474,18 +597,19 @@ func TestIntegrationUserAuthToken(t *testing.T) {
t.Run("When populating userAuthToken from UserToken should copy all properties", func(t *testing.T) {
ut := auth.UserToken{
Id: 1,
UserId: 2,
AuthToken: "a",
PrevAuthToken: "b",
UserAgent: "c",
ClientIp: "d",
AuthTokenSeen: true,
SeenAt: 3,
RotatedAt: 4,
CreatedAt: 5,
UpdatedAt: 6,
UnhashedToken: "e",
Id: 1,
UserId: 2,
AuthToken: "a",
PrevAuthToken: "b",
UserAgent: "c",
ClientIp: "d",
AuthTokenSeen: true,
SeenAt: 3,
RotatedAt: 4,
CreatedAt: 5,
UpdatedAt: 6,
UnhashedToken: "e",
ExternalSessionId: 7,
}
utBytes, err := json.Marshal(ut)
require.Nil(t, err)
@ -507,18 +631,19 @@ func TestIntegrationUserAuthToken(t *testing.T) {
t.Run("When populating userToken from userAuthToken should copy all properties", func(t *testing.T) {
uat := userAuthToken{
Id: 1,
UserId: 2,
AuthToken: "a",
PrevAuthToken: "b",
UserAgent: "c",
ClientIp: "d",
AuthTokenSeen: true,
SeenAt: 3,
RotatedAt: 4,
CreatedAt: 5,
UpdatedAt: 6,
UnhashedToken: "e",
Id: 1,
UserId: 2,
AuthToken: "a",
PrevAuthToken: "b",
UserAgent: "c",
ClientIp: "d",
AuthTokenSeen: true,
SeenAt: 3,
RotatedAt: 4,
CreatedAt: 5,
UpdatedAt: 6,
UnhashedToken: "e",
ExternalSessionId: 7,
}
uatBytes, err := json.Marshal(uat)
require.Nil(t, err)
@ -551,22 +676,27 @@ func createTestContext(t *testing.T) *testContext {
TokenRotationIntervalMinutes: 10,
}
extSessionStore := provideExternalSessionStore(sqlstore, &fakes.FakeSecretsService{}, tracing.InitializeTracerForTest())
tokenService := &UserAuthTokenService{
sqlStore: sqlstore,
cfg: cfg,
log: log.New("test-logger"),
singleflight: new(singleflight.Group),
sqlStore: sqlstore,
cfg: cfg,
log: log.New("test-logger"),
singleflight: new(singleflight.Group),
externalSessionStore: extSessionStore,
}
return &testContext{
sqlstore: sqlstore,
tokenService: tokenService,
sqlstore: sqlstore,
tokenService: tokenService,
extSessionStore: &extSessionStore,
}
}
type testContext struct {
sqlstore db.DB
tokenService *UserAuthTokenService
sqlstore db.DB
tokenService *UserAuthTokenService
extSessionStore *auth.ExternalSessionStore
}
func (c *testContext) getAuthTokenByID(id int64) (*userAuthToken, error) {
@ -585,6 +715,22 @@ func (c *testContext) getAuthTokenByID(id int64) (*userAuthToken, error) {
return res, err
}
func (c *testContext) getExternalSessionByID(ID int64) (*auth.ExternalSession, error) {
var res *auth.ExternalSession
err := c.sqlstore.WithDbSession(context.Background(), func(sess *db.Session) error {
var t auth.ExternalSession
found, err := sess.ID(ID).Get(&t)
if err != nil || !found {
return err
}
res = &t
return nil
})
return res, err
}
func (c *testContext) updateRotatedAt(id, rotatedAt int64) (bool, error) {
hasRowsAffected := false
err := c.sqlstore.WithDbSession(context.Background(), func(sess *db.Session) error {
@ -609,8 +755,11 @@ func TestIntegrationTokenCount(t *testing.T) {
user := &user.User{ID: int64(10)}
createToken := func() *auth.UserToken {
userToken, err := ctx.tokenService.CreateToken(context.Background(), user,
net.ParseIP("192.168.10.11"), "some user agent")
userToken, err := ctx.tokenService.CreateToken(context.Background(), &auth.CreateTokenCommand{
User: user,
ClientIP: net.ParseIP("192.168.10.11"),
UserAgent: "some user agent",
})
require.Nil(t, err)
require.NotNil(t, userToken)
require.False(t, userToken.AuthTokenSeen)
@ -637,3 +786,108 @@ func TestIntegrationTokenCount(t *testing.T) {
require.Nil(t, err)
require.Equal(t, int64(0), count)
}
func TestRevokeAllUserTokens(t *testing.T) {
t.Run("should not fail if the external sessions could not be removed", func(t *testing.T) {
ctx := createTestContext(t)
usr := &user.User{ID: int64(10)}
// Mock the external session store to return an error
mockExternalSessionStore := &authtest.MockExternalSessionStore{}
mockExternalSessionStore.On("Create", mock.Anything, mock.IsType(&auth.ExternalSession{})).Run(func(args mock.Arguments) {
extSession := args.Get(1).(*auth.ExternalSession)
extSession.ID = 1
}).Return(nil)
mockExternalSessionStore.On("DeleteExternalSessionsByUserID", mock.Anything, usr.ID).Return(errors.New("some error"))
ctx.tokenService.externalSessionStore = mockExternalSessionStore
userToken, err := ctx.tokenService.CreateToken(context.Background(), &auth.CreateTokenCommand{
User: usr,
ClientIP: net.ParseIP("192.168.10.11"),
UserAgent: "some user agent",
ExternalSession: &auth.ExternalSession{UserID: usr.ID, AuthModule: "test", UserAuthID: 1},
})
require.Nil(t, err)
require.NotNil(t, userToken)
err = ctx.tokenService.RevokeAllUserTokens(context.Background(), usr.ID)
require.Nil(t, err)
model, err := ctx.getAuthTokenByID(userToken.Id)
require.Nil(t, err)
require.Nil(t, model)
})
}
func TestRevokeToken(t *testing.T) {
t.Run("should not fail if the external sessions could not be removed", func(t *testing.T) {
ctx := createTestContext(t)
usr := &user.User{ID: int64(10)}
mockExternalSessionStore := &authtest.MockExternalSessionStore{}
mockExternalSessionStore.On("Create", mock.Anything, mock.IsType(&auth.ExternalSession{})).Run(func(args mock.Arguments) {
extSession := args.Get(1).(*auth.ExternalSession)
extSession.ID = 2
}).Return(nil)
mockExternalSessionStore.On("Delete", mock.Anything, int64(2)).Return(errors.New("some error"))
ctx.tokenService.externalSessionStore = mockExternalSessionStore
userToken, err := ctx.tokenService.CreateToken(context.Background(), &auth.CreateTokenCommand{
User: usr,
ClientIP: net.ParseIP("192.168.10.11"),
UserAgent: "some user agent",
ExternalSession: &auth.ExternalSession{UserID: usr.ID, AuthModule: "test", UserAuthID: 1},
})
require.Nil(t, err)
require.NotNil(t, userToken)
err = ctx.tokenService.RevokeToken(context.Background(), userToken, false)
require.Nil(t, err)
model, err := ctx.getAuthTokenByID(userToken.Id)
require.Nil(t, err)
require.Nil(t, model)
})
}
func TestBatchRevokeAllUserTokens(t *testing.T) {
t.Run("should not fail if the external sessions could not be removed", func(t *testing.T) {
ctx := createTestContext(t)
userIds := []int64{1, 2, 3}
mockExternalSessionStore := &authtest.MockExternalSessionStore{}
mockExternalSessionStore.On("BatchDeleteExternalSessionsByUserIDs", mock.Anything, userIds).Return(errors.New("some error"))
ctr := int64(0)
mockExternalSessionStore.On("Create", mock.Anything, mock.IsType(&auth.ExternalSession{})).Run(func(args mock.Arguments) {
extSession := args.Get(1).(*auth.ExternalSession)
ctr += 1
extSession.ID = ctr
}).Return(nil)
ctx.tokenService.externalSessionStore = mockExternalSessionStore
for _, userID := range userIds {
usr := &user.User{ID: userID}
userToken, err := ctx.tokenService.CreateToken(context.Background(), &auth.CreateTokenCommand{
User: usr,
ClientIP: net.ParseIP("192.168.10.11"),
UserAgent: "some user agent",
ExternalSession: &auth.ExternalSession{UserID: usr.ID, AuthModule: "test", UserAuthID: 1},
})
require.Nil(t, err)
require.NotNil(t, userToken)
}
// Batch revoke all user tokens
err := ctx.tokenService.BatchRevokeAllUserTokens(context.Background(), userIds)
require.Nil(t, err)
// Verify that the tokens have been revoked
for _, userID := range userIds {
tokens, err := ctx.tokenService.GetUserTokens(context.Background(), userID)
require.Nil(t, err)
require.Equal(t, 0, len(tokens))
}
})
}

View File

@ -0,0 +1,244 @@
package authimpl
import (
"context"
"crypto/sha256"
"encoding/base64"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/secrets"
)
var _ auth.ExternalSessionStore = (*store)(nil)
type store struct {
sqlStore db.DB
secretsService secrets.Service
tracer tracing.Tracer
}
func provideExternalSessionStore(sqlStore db.DB, secretService secrets.Service, tracer tracing.Tracer) auth.ExternalSessionStore {
return &store{
sqlStore: sqlStore,
secretsService: secretService,
tracer: tracer,
}
}
func (s *store) Get(ctx context.Context, extSessionID int64) (*auth.ExternalSession, error) {
ctx, span := s.tracer.Start(ctx, "externalsession.Get")
defer span.End()
externalSession := &auth.ExternalSession{ID: extSessionID}
err := s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
found, err := sess.Get(externalSession)
if err != nil {
return err
}
if !found {
return auth.ErrExternalSessionNotFound
}
return nil
})
if err != nil {
return nil, err
}
err = s.decryptSecrets(externalSession)
if err != nil {
return nil, err
}
return externalSession, nil
}
func (s *store) List(ctx context.Context, query *auth.ListExternalSessionQuery) ([]*auth.ExternalSession, error) {
ctx, span := s.tracer.Start(ctx, "externalsession.List")
defer span.End()
externalSession := &auth.ExternalSession{}
if query.ID != 0 {
externalSession.ID = query.ID
}
hash := sha256.New()
if query.SessionID != "" {
hash.Write([]byte(query.SessionID))
externalSession.SessionIDHash = base64.RawStdEncoding.EncodeToString(hash.Sum(nil))
}
if query.NameID != "" {
hash.Reset()
hash.Write([]byte(query.NameID))
externalSession.NameIDHash = base64.RawStdEncoding.EncodeToString(hash.Sum(nil))
}
queryResult := make([]*auth.ExternalSession, 0)
err := s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
return sess.Find(&queryResult, externalSession)
})
if err != nil {
return nil, err
}
for _, extSession := range queryResult {
err := s.decryptSecrets(extSession)
if err != nil {
return nil, err
}
}
return queryResult, nil
}
func (s *store) Create(ctx context.Context, extSession *auth.ExternalSession) error {
ctx, span := s.tracer.Start(ctx, "externalsession.Create")
defer span.End()
var err error
clone := extSession.Clone()
clone.AccessToken, err = s.encryptAndEncode(extSession.AccessToken)
if err != nil {
return err
}
clone.RefreshToken, err = s.encryptAndEncode(extSession.RefreshToken)
if err != nil {
return err
}
clone.IDToken, err = s.encryptAndEncode(extSession.IDToken)
if err != nil {
return err
}
if extSession.NameID != "" {
hash := sha256.New()
hash.Write([]byte(extSession.NameID))
clone.NameIDHash = base64.RawStdEncoding.EncodeToString(hash.Sum(nil))
}
clone.NameID, err = s.encryptAndEncode(extSession.NameID)
if err != nil {
return err
}
if extSession.SessionID != "" {
hash := sha256.New()
hash.Write([]byte(extSession.SessionID))
clone.SessionIDHash = base64.RawStdEncoding.EncodeToString(hash.Sum(nil))
}
clone.SessionID, err = s.encryptAndEncode(extSession.SessionID)
if err != nil {
return err
}
err = s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
_, err := sess.Insert(clone)
return err
})
if err != nil {
return err
}
extSession.ID = clone.ID
return nil
}
func (s *store) Delete(ctx context.Context, ID int64) error {
ctx, span := s.tracer.Start(ctx, "externalsession.Delete")
defer span.End()
externalSession := &auth.ExternalSession{ID: ID}
err := s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
_, err := sess.Delete(externalSession)
return err
})
return err
}
func (s *store) DeleteExternalSessionsByUserID(ctx context.Context, userID int64) error {
ctx, span := s.tracer.Start(ctx, "externalsession.DeleteExternalSessionsByUserID")
defer span.End()
externalSession := &auth.ExternalSession{UserID: userID}
err := s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
_, err := sess.Delete(externalSession)
return err
})
return err
}
func (s *store) BatchDeleteExternalSessionsByUserIDs(ctx context.Context, userIDs []int64) error {
ctx, span := s.tracer.Start(ctx, "externalsession.BatchDeleteExternalSessionsByUserIDs")
defer span.End()
externalSession := &auth.ExternalSession{}
err := s.sqlStore.WithDbSession(ctx, func(sess *db.Session) error {
_, err := sess.In("user_id", userIDs).Delete(externalSession)
return err
})
return err
}
func (s *store) decryptSecrets(extSession *auth.ExternalSession) error {
var err error
extSession.AccessToken, err = s.decodeAndDecrypt(extSession.AccessToken)
if err != nil {
return err
}
extSession.RefreshToken, err = s.decodeAndDecrypt(extSession.RefreshToken)
if err != nil {
return err
}
extSession.IDToken, err = s.decodeAndDecrypt(extSession.IDToken)
if err != nil {
return err
}
extSession.NameID, err = s.decodeAndDecrypt(extSession.NameID)
if err != nil {
return err
}
extSession.SessionID, err = s.decodeAndDecrypt(extSession.SessionID)
if err != nil {
return err
}
return nil
}
func (s *store) encryptAndEncode(str string) (string, error) {
if str == "" {
return "", nil
}
encrypted, err := s.secretsService.Encrypt(context.Background(), []byte(str), secrets.WithoutScope())
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(encrypted), nil
}
func (s *store) decodeAndDecrypt(str string) (string, error) {
// Bail out if empty string since it'll cause a segfault in Decrypt
if str == "" {
return "", nil
}
decoded, err := base64.StdEncoding.DecodeString(str)
if err != nil {
return "", err
}
decrypted, err := s.secretsService.Decrypt(context.Background(), decoded)
if err != nil {
return "", err
}
return string(decrypted), nil
}

View File

@ -0,0 +1,228 @@
package authimpl
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
"github.com/stretchr/testify/require"
)
func TestGetExternalSession(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Run("returns existing external session", func(t *testing.T) {
store := setupTest(t)
extSession := &auth.ExternalSession{
AccessToken: "access-token",
}
err := store.Create(context.Background(), extSession)
require.NoError(t, err)
actual, err := store.Get(context.Background(), extSession.ID)
require.NoError(t, err)
require.EqualValues(t, extSession.ID, actual.ID)
require.EqualValues(t, extSession.AccessToken, actual.AccessToken)
})
t.Run("returns not found if the external session is missing", func(t *testing.T) {
store := setupTest(t)
_, err := store.Get(context.Background(), 999)
require.ErrorIs(t, err, auth.ErrExternalSessionNotFound)
})
}
func TestFindExternalSessions(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Run("returns external sessions by ID", func(t *testing.T) {
store := setupTest(t)
extSession := &auth.ExternalSession{
AccessToken: "access-token",
}
err := store.Create(context.Background(), extSession)
require.NoError(t, err)
query := &auth.ListExternalSessionQuery{ID: extSession.ID}
actual, err := store.List(context.Background(), query)
require.NoError(t, err)
require.Len(t, actual, 1)
require.EqualValues(t, extSession.ID, actual[0].ID)
require.EqualValues(t, extSession.AccessToken, actual[0].AccessToken)
})
t.Run("returns external sessions by SessionID", func(t *testing.T) {
store := setupTest(t)
extSession := &auth.ExternalSession{
SessionID: "session-index",
}
err := store.Create(context.Background(), extSession)
require.NoError(t, err)
query := &auth.ListExternalSessionQuery{SessionID: extSession.SessionID}
actual, err := store.List(context.Background(), query)
require.NoError(t, err)
require.Len(t, actual, 1)
require.EqualValues(t, extSession.ID, actual[0].ID)
require.EqualValues(t, extSession.SessionID, actual[0].SessionID)
})
t.Run("returns external sessions by NameID", func(t *testing.T) {
store := setupTest(t)
extSession := &auth.ExternalSession{
NameID: "name-id",
}
err := store.Create(context.Background(), extSession)
require.NoError(t, err)
query := &auth.ListExternalSessionQuery{NameID: extSession.NameID}
actual, err := store.List(context.Background(), query)
require.NoError(t, err)
require.Len(t, actual, 1)
require.EqualValues(t, extSession.ID, actual[0].ID)
require.EqualValues(t, extSession.NameID, actual[0].NameID)
})
t.Run("returns empty result if no external sessions match the query", func(t *testing.T) {
store := setupTest(t)
query := &auth.ListExternalSessionQuery{ID: 999}
actual, err := store.List(context.Background(), query)
require.NoError(t, err)
require.Len(t, actual, 0)
})
}
func TestDeleteExternalSessionsByUserID(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Run("deletes all external sessions for a given user ID", func(t *testing.T) {
store := setupTest(t)
userID := int64(1)
extSession1 := &auth.ExternalSession{
UserID: userID,
AccessToken: "access-token-1",
}
extSession2 := &auth.ExternalSession{
UserID: userID,
AccessToken: "access-token-2",
}
err := store.Create(context.Background(), extSession1)
require.NoError(t, err)
err = store.Create(context.Background(), extSession2)
require.NoError(t, err)
err = store.DeleteExternalSessionsByUserID(context.Background(), userID)
require.NoError(t, err)
query := &auth.ListExternalSessionQuery{}
actual, err := store.List(context.Background(), query)
require.NoError(t, err)
require.Len(t, actual, 0)
})
t.Run("returns no error if no external sessions exist for the given user ID", func(t *testing.T) {
store := setupTest(t)
userID := int64(999)
err := store.DeleteExternalSessionsByUserID(context.Background(), userID)
require.NoError(t, err)
})
}
func TestDeleteExternalSession(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Run("deletes an existing external session", func(t *testing.T) {
store := setupTest(t)
extSession := &auth.ExternalSession{
AccessToken: "access-token",
}
err := store.Create(context.Background(), extSession)
require.NoError(t, err)
err = store.Delete(context.Background(), extSession.ID)
require.NoError(t, err)
_, err = store.Get(context.Background(), extSession.ID)
require.ErrorIs(t, err, auth.ErrExternalSessionNotFound)
})
t.Run("returns no error if the external session does not exist", func(t *testing.T) {
store := setupTest(t)
err := store.Delete(context.Background(), 999)
require.NoError(t, err)
})
}
func TestBatchDeleteExternalSessionsByUserIDs(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
t.Run("deletes all external sessions for given user IDs", func(t *testing.T) {
store := setupTest(t)
userID1 := int64(1)
userID2 := int64(2)
extSession1 := &auth.ExternalSession{
UserID: userID1,
AccessToken: "access-token-1",
}
extSession2 := &auth.ExternalSession{
UserID: userID2,
AccessToken: "access-token-2",
}
err := store.Create(context.Background(), extSession1)
require.NoError(t, err)
err = store.Create(context.Background(), extSession2)
require.NoError(t, err)
err = store.BatchDeleteExternalSessionsByUserIDs(context.Background(), []int64{userID1, userID2})
require.NoError(t, err)
query := &auth.ListExternalSessionQuery{}
actual, err := store.List(context.Background(), query)
require.NoError(t, err)
require.Len(t, actual, 0)
})
t.Run("returns no error if no external sessions exist for the given user IDs", func(t *testing.T) {
store := setupTest(t)
err := store.BatchDeleteExternalSessionsByUserIDs(context.Background(), []int64{999, 1000})
require.NoError(t, err)
})
}
func setupTest(t *testing.T) *store {
sqlStore := db.InitTestDB(t)
secretService := fakes.NewFakeSecretsService()
tracer := tracing.InitializeTracerForTest()
externalSessionStore := provideExternalSessionStore(sqlStore, secretService, tracer).(*store)
return externalSessionStore
}

View File

@ -7,19 +7,20 @@ import (
)
type userAuthToken struct {
Id int64
UserId int64
AuthToken string
PrevAuthToken string
UserAgent string
ClientIp string
AuthTokenSeen bool
SeenAt int64
RotatedAt int64
CreatedAt int64
UpdatedAt int64
RevokedAt int64
UnhashedToken string `xorm:"-"`
Id int64
UserId int64
AuthToken string
PrevAuthToken string
UserAgent string
ClientIp string
AuthTokenSeen bool
SeenAt int64
RotatedAt int64
CreatedAt int64
UpdatedAt int64
RevokedAt int64
UnhashedToken string `xorm:"-"`
ExternalSessionId int64
}
func userAuthTokenFromUserToken(ut *auth.UserToken) (*userAuthToken, error) {
@ -46,6 +47,7 @@ func (uat *userAuthToken) fromUserToken(ut *auth.UserToken) error {
uat.UpdatedAt = ut.UpdatedAt
uat.RevokedAt = ut.RevokedAt
uat.UnhashedToken = ut.UnhashedToken
uat.ExternalSessionId = ut.ExternalSessionId
return nil
}
@ -68,5 +70,6 @@ func (uat *userAuthToken) toUserToken(ut *auth.UserToken) error {
ut.UpdatedAt = uat.UpdatedAt
ut.RevokedAt = uat.RevokedAt
ut.UnhashedToken = uat.UnhashedToken
ut.ExternalSessionId = uat.ExternalSessionId
return nil
}

View File

@ -16,6 +16,9 @@ func (s *UserAuthTokenService) Run(ctx context.Context) error {
if _, err := s.deleteExpiredTokens(ctx, maxInactiveLifetime, maxLifetime); err != nil {
s.log.Error("An error occurred while deleting expired tokens", "err", err)
}
if err := s.deleteOrphanedExternalSessions(ctx); err != nil {
s.log.Error("An error occurred while deleting orphaned external sessions", "err", err)
}
})
if err != nil {
s.log.Error("Failed to lock and execute cleanup of expired auth token", "error", err)
@ -28,6 +31,9 @@ func (s *UserAuthTokenService) Run(ctx context.Context) error {
if _, err := s.deleteExpiredTokens(ctx, maxInactiveLifetime, maxLifetime); err != nil {
s.log.Error("An error occurred while deleting expired tokens", "err", err)
}
if err := s.deleteOrphanedExternalSessions(ctx); err != nil {
s.log.Error("An error occurred while deleting orphaned external sessions", "err", err)
}
})
if err != nil {
s.log.Error("Failed to lock and execute cleanup of expired auth token", "error", err)
@ -66,3 +72,29 @@ func (s *UserAuthTokenService) deleteExpiredTokens(ctx context.Context, maxInact
return affected, err
}
func (s *UserAuthTokenService) deleteOrphanedExternalSessions(ctx context.Context) error {
s.log.Debug("Starting cleanup of external sessions")
var affected int64
err := s.sqlStore.WithDbSession(ctx, func(dbSession *db.Session) error {
sql := `DELETE FROM user_external_session WHERE NOT EXISTS (SELECT 1 FROM user_auth_token WHERE user_external_session.id = user_auth_token.external_session_id)`
res, err := dbSession.Exec(sql)
if err != nil {
return err
}
affected, err = res.RowsAffected()
if err != nil {
s.log.Error("Failed to cleanup orphaned external sessions", "error", err)
return nil
}
s.log.Debug("Cleanup of orphaned external sessions done", "count", affected)
return nil
})
return err
}

View File

@ -9,9 +9,14 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/services/auth"
)
func TestUserAuthTokenCleanup(t *testing.T) {
func TestIntegrationUserAuthTokenCleanup(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
setup := func() *testContext {
ctx := createTestContext(t)
maxInactiveLifetime, _ := time.ParseDuration("168h")
@ -75,3 +80,61 @@ func TestUserAuthTokenCleanup(t *testing.T) {
require.Equal(t, int64(3), affected)
})
}
func TestIntegrationOrphanedExternalSessionsCleanup(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
setup := func() *testContext {
ctx := createTestContext(t)
return ctx
}
insertExternalSession := func(ctx *testContext, id int64) {
es := &auth.ExternalSession{ID: id, UserAuthID: 1, UserID: 1}
err := ctx.sqlstore.WithDbSession(context.Background(), func(sess *db.Session) error {
_, err := sess.Insert(es)
require.Nil(t, err)
return nil
})
require.NoError(t, err)
}
insertAuthToken := func(ctx *testContext, token string, externalSessionId int64) {
ut := userAuthToken{AuthToken: token, PrevAuthToken: fmt.Sprintf("old%s", token), ExternalSessionId: externalSessionId}
err := ctx.sqlstore.WithDbSession(context.Background(), func(sess *db.Session) error {
_, err := sess.Insert(&ut)
require.Nil(t, err)
return nil
})
require.NoError(t, err)
}
t.Run("should delete orphaned external sessions", func(t *testing.T) {
ctx := setup()
// insert three external sessions
for i := int64(1); i <= 3; i++ {
insertExternalSession(ctx, i)
}
// insert two auth tokens linked to external sessions
insertAuthToken(ctx, "token1", 1)
insertAuthToken(ctx, "token2", 2)
// delete orphaned external sessions
err := ctx.tokenService.deleteOrphanedExternalSessions(context.Background())
require.NoError(t, err)
// verify that only the orphaned external session is deleted
var count int64
err = ctx.sqlstore.WithDbSession(context.Background(), func(sess *db.Session) error {
count, err = sess.Count(&auth.ExternalSession{})
require.Nil(t, err)
return nil
})
require.NoError(t, err)
require.Equal(t, int64(2), count)
})
}

View File

@ -0,0 +1,162 @@
// Code generated by mockery v2.42.1. DO NOT EDIT.
package authtest
import (
context "context"
auth "github.com/grafana/grafana/pkg/services/auth"
mock "github.com/stretchr/testify/mock"
)
// MockExternalSessionStore is an autogenerated mock type for the ExternalSessionStore type
type MockExternalSessionStore struct {
mock.Mock
}
// BatchDeleteExternalSessionsByUserIDs provides a mock function with given fields: ctx, userIDs
func (_m *MockExternalSessionStore) BatchDeleteExternalSessionsByUserIDs(ctx context.Context, userIDs []int64) error {
ret := _m.Called(ctx, userIDs)
if len(ret) == 0 {
panic("no return value specified for BatchDeleteExternalSessionsByUserIDs")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, []int64) error); ok {
r0 = rf(ctx, userIDs)
} else {
r0 = ret.Error(0)
}
return r0
}
// Create provides a mock function with given fields: ctx, extSesion
func (_m *MockExternalSessionStore) Create(ctx context.Context, extSesion *auth.ExternalSession) error {
ret := _m.Called(ctx, extSesion)
if len(ret) == 0 {
panic("no return value specified for Create")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *auth.ExternalSession) error); ok {
r0 = rf(ctx, extSesion)
} else {
r0 = ret.Error(0)
}
return r0
}
// Delete provides a mock function with given fields: ctx, ID
func (_m *MockExternalSessionStore) Delete(ctx context.Context, ID int64) error {
ret := _m.Called(ctx, ID)
if len(ret) == 0 {
panic("no return value specified for Delete")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, ID)
} else {
r0 = ret.Error(0)
}
return r0
}
// DeleteExternalSessionsByUserID provides a mock function with given fields: ctx, userID
func (_m *MockExternalSessionStore) DeleteExternalSessionsByUserID(ctx context.Context, userID int64) error {
ret := _m.Called(ctx, userID)
if len(ret) == 0 {
panic("no return value specified for DeleteExternalSessionsByUserID")
}
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok {
r0 = rf(ctx, userID)
} else {
r0 = ret.Error(0)
}
return r0
}
// Get provides a mock function with given fields: ctx, ID
func (_m *MockExternalSessionStore) Get(ctx context.Context, ID int64) (*auth.ExternalSession, error) {
ret := _m.Called(ctx, ID)
if len(ret) == 0 {
panic("no return value specified for Get")
}
var r0 *auth.ExternalSession
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, int64) (*auth.ExternalSession, error)); ok {
return rf(ctx, ID)
}
if rf, ok := ret.Get(0).(func(context.Context, int64) *auth.ExternalSession); ok {
r0 = rf(ctx, ID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*auth.ExternalSession)
}
}
if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok {
r1 = rf(ctx, ID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// List provides a mock function with given fields: ctx, query
func (_m *MockExternalSessionStore) List(ctx context.Context, query *auth.ListExternalSessionQuery) ([]*auth.ExternalSession, error) {
ret := _m.Called(ctx, query)
if len(ret) == 0 {
panic("no return value specified for List")
}
var r0 []*auth.ExternalSession
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, *auth.ListExternalSessionQuery) ([]*auth.ExternalSession, error)); ok {
return rf(ctx, query)
}
if rf, ok := ret.Get(0).(func(context.Context, *auth.ListExternalSessionQuery) []*auth.ExternalSession); ok {
r0 = rf(ctx, query)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*auth.ExternalSession)
}
}
if rf, ok := ret.Get(1).(func(context.Context, *auth.ListExternalSessionQuery) error); ok {
r1 = rf(ctx, query)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// NewMockExternalSessionStore creates a new instance of MockExternalSessionStore. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockExternalSessionStore(t interface {
mock.TestingT
Cleanup(func())
}) *MockExternalSessionStore {
mock := &MockExternalSessionStore{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@ -11,26 +11,28 @@ import (
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/user"
)
type FakeUserAuthTokenService struct {
CreateTokenProvider func(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*auth.UserToken, error)
RotateTokenProvider func(ctx context.Context, cmd auth.RotateCommand) (*auth.UserToken, error)
TryRotateTokenProvider func(ctx context.Context, token *auth.UserToken, clientIP net.IP, userAgent string) (bool, *auth.UserToken, error)
LookupTokenProvider func(ctx context.Context, unhashedToken string) (*auth.UserToken, error)
RevokeTokenProvider func(ctx context.Context, token *auth.UserToken, soft bool) error
RevokeAllUserTokensProvider func(ctx context.Context, userID int64) error
ActiveTokenCountProvider func(ctx context.Context, userID *int64) (int64, error)
GetUserTokenProvider func(ctx context.Context, userID, userTokenID int64) (*auth.UserToken, error)
GetUserTokensProvider func(ctx context.Context, userID int64) ([]*auth.UserToken, error)
GetUserRevokedTokensProvider func(ctx context.Context, userID int64) ([]*auth.UserToken, error)
BatchRevokedTokenProvider func(ctx context.Context, userIDs []int64) error
CreateTokenProvider func(ctx context.Context, cmd *auth.CreateTokenCommand) (*auth.UserToken, error)
RotateTokenProvider func(ctx context.Context, cmd auth.RotateCommand) (*auth.UserToken, error)
GetTokenByExternalSessionIDProvider func(ctx context.Context, externalSessionID int64) (*auth.UserToken, error)
GetExternalSessionProvider func(ctx context.Context, externalSessionID int64) (*auth.ExternalSession, error)
FindExternalSessionsProvider func(ctx context.Context, query *auth.ListExternalSessionQuery) ([]*auth.ExternalSession, error)
TryRotateTokenProvider func(ctx context.Context, token *auth.UserToken, clientIP net.IP, userAgent string) (bool, *auth.UserToken, error)
LookupTokenProvider func(ctx context.Context, unhashedToken string) (*auth.UserToken, error)
RevokeTokenProvider func(ctx context.Context, token *auth.UserToken, soft bool) error
RevokeAllUserTokensProvider func(ctx context.Context, userID int64) error
ActiveTokenCountProvider func(ctx context.Context, userID *int64) (int64, error)
GetUserTokenProvider func(ctx context.Context, userID, userTokenID int64) (*auth.UserToken, error)
GetUserTokensProvider func(ctx context.Context, userID int64) ([]*auth.UserToken, error)
GetUserRevokedTokensProvider func(ctx context.Context, userID int64) ([]*auth.UserToken, error)
BatchRevokedTokenProvider func(ctx context.Context, userIDs []int64) error
}
func NewFakeUserAuthTokenService() *FakeUserAuthTokenService {
return &FakeUserAuthTokenService{
CreateTokenProvider: func(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*auth.UserToken, error) {
CreateTokenProvider: func(ctx context.Context, cmd *auth.CreateTokenCommand) (*auth.UserToken, error) {
return &auth.UserToken{
UserId: 0,
UnhashedToken: "",
@ -72,14 +74,26 @@ func (s *FakeUserAuthTokenService) Init() error {
return nil
}
func (s *FakeUserAuthTokenService) CreateToken(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*auth.UserToken, error) {
return s.CreateTokenProvider(context.Background(), user, clientIP, userAgent)
func (s *FakeUserAuthTokenService) CreateToken(ctx context.Context, cmd *auth.CreateTokenCommand) (*auth.UserToken, error) {
return s.CreateTokenProvider(context.Background(), cmd)
}
func (s *FakeUserAuthTokenService) RotateToken(ctx context.Context, cmd auth.RotateCommand) (*auth.UserToken, error) {
return s.RotateTokenProvider(ctx, cmd)
}
func (s *FakeUserAuthTokenService) GetTokenByExternalSessionID(ctx context.Context, externalSessionID int64) (*auth.UserToken, error) {
return s.GetTokenByExternalSessionIDProvider(ctx, externalSessionID)
}
func (s *FakeUserAuthTokenService) GetExternalSession(ctx context.Context, externalSessionID int64) (*auth.ExternalSession, error) {
return s.GetExternalSessionProvider(ctx, externalSessionID)
}
func (s *FakeUserAuthTokenService) FindExternalSessions(ctx context.Context, query *auth.ListExternalSessionQuery) ([]*auth.ExternalSession, error) {
return s.FindExternalSessionsProvider(context.Background(), query)
}
func (s *FakeUserAuthTokenService) LookupToken(ctx context.Context, unhashedToken string) (*auth.UserToken, error) {
return s.LookupTokenProvider(context.Background(), unhashedToken)
}

View File

@ -0,0 +1,66 @@
package auth
import (
"context"
"time"
)
type ExternalSession struct {
ID int64 `xorm:"pk autoincr 'id'"`
UserID int64 `xorm:"user_id"`
UserAuthID int64 `xorm:"user_auth_id"`
AuthModule string `xorm:"auth_module"`
AccessToken string `xorm:"access_token"`
IDToken string `xorm:"id_token"`
RefreshToken string `xorm:"refresh_token"`
SessionID string `xorm:"session_id"`
SessionIDHash string `xorm:"session_id_hash"`
NameID string `xorm:"name_id"`
NameIDHash string `xorm:"name_id_hash"`
ExpiresAt time.Time `xorm:"expires_at"`
CreatedAt time.Time `xorm:"created 'created_at'"`
}
func (e *ExternalSession) TableName() string {
return "user_external_session"
}
func (e *ExternalSession) Clone() *ExternalSession {
return &ExternalSession{
ID: e.ID,
UserID: e.UserID,
UserAuthID: e.UserAuthID,
AuthModule: e.AuthModule,
AccessToken: e.AccessToken,
IDToken: e.IDToken,
RefreshToken: e.RefreshToken,
SessionID: e.SessionID,
SessionIDHash: e.SessionIDHash,
NameID: e.NameID,
NameIDHash: e.NameIDHash,
ExpiresAt: e.ExpiresAt,
CreatedAt: e.CreatedAt,
}
}
type ListExternalSessionQuery struct {
ID int64
NameID string
SessionID string
}
//go:generate mockery --name ExternalSessionStore --structname MockExternalSessionStore --outpkg authtest --filename external_session_store_mock.go --output ./authtest/
type ExternalSessionStore interface {
// Get returns the external session
Get(ctx context.Context, ID int64) (*ExternalSession, error)
// List returns all external sessions fπor the given query
List(ctx context.Context, query *ListExternalSessionQuery) ([]*ExternalSession, error)
// Create creates a new external session for a user
Create(ctx context.Context, extSesion *ExternalSession) error
// Delete deletes an external session
Delete(ctx context.Context, ID int64) error
// DeleteExternalSessionsByUserID deletes an external session
DeleteExternalSessionsByUserID(ctx context.Context, userID int64) error
// BatchDeleteExternalSessionsByUserIDs deletes external sessions by user IDs
BatchDeleteExternalSessionsByUserIDs(ctx context.Context, userIDs []int64) error
}

View File

@ -22,6 +22,7 @@ import (
"github.com/grafana/grafana/pkg/services/auth"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/authn/clients"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
@ -52,8 +53,8 @@ func ProvideIdentitySynchronizer(s *Service) authn.IdentitySynchronizer {
}
func ProvideService(
cfg *setting.Cfg, tracer tracing.Tracer,
sessionService auth.UserTokenService, usageStats usagestats.Service, registerer prometheus.Registerer,
cfg *setting.Cfg, tracer tracing.Tracer, sessionService auth.UserTokenService,
usageStats usagestats.Service, registerer prometheus.Registerer, authTokenService login.AuthInfoService,
) *Service {
s := &Service{
log: log.New("authn.service"),
@ -64,6 +65,7 @@ func ProvideService(
tracer: tracer,
metrics: newMetrics(registerer),
sessionService: sessionService,
authTokenService: authTokenService,
preLogoutHooks: newQueue[authn.PreLogoutHookFn](),
postAuthHooks: newQueue[authn.PostAuthHookFn](),
postLoginHooks: newQueue[authn.PostLoginHookFn](),
@ -85,7 +87,8 @@ type Service struct {
tracer tracing.Tracer
metrics *metrics
sessionService auth.UserTokenService
sessionService auth.UserTokenService
authTokenService login.AuthInfoService
// postAuthHooks are called after a successful authentication. They can modify the identity.
postAuthHooks *queue[authn.PostAuthHookFn]
@ -238,7 +241,9 @@ func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (i
s.log.FromContext(ctx).Debug("Failed to parse ip from address", "client", c.Name(), "id", id.ID, "addr", addr, "error", err)
}
sessionToken, err := s.sessionService.CreateToken(ctx, &user.User{ID: userID}, ip, r.HTTPRequest.UserAgent())
externalSession := s.resolveExternalSessionFromIdentity(ctx, id, userID)
sessionToken, err := s.sessionService.CreateToken(ctx, &auth.CreateTokenCommand{User: &user.User{ID: userID}, ClientIP: ip, UserAgent: r.HTTPRequest.UserAgent(), ExternalSession: externalSession})
if err != nil {
s.metrics.failedLogin.WithLabelValues(client).Inc()
s.log.FromContext(ctx).Error("Failed to create session", "client", client, "id", id.ID, "err", err)
@ -403,7 +408,8 @@ func (s *Service) resolveIdenity(ctx context.Context, orgID int64, typedID strin
AllowGlobalOrg: true,
FetchSyncedUser: true,
SyncPermissions: true,
}}, nil
},
}, nil
}
if claims.IsIdentityType(t, claims.TypeServiceAccount) {
@ -415,7 +421,8 @@ func (s *Service) resolveIdenity(ctx context.Context, orgID int64, typedID strin
AllowGlobalOrg: true,
FetchSyncedUser: true,
SyncPermissions: true,
}}, nil
},
}, nil
}
resolver, ok := s.idenityResolverClients[string(t)]
@ -482,3 +489,35 @@ func orgIDFromHeader(req *http.Request) int64 {
}
return id
}
func (s *Service) resolveExternalSessionFromIdentity(ctx context.Context, identity *authn.Identity, userID int64) *auth.ExternalSession {
if identity.OAuthToken == nil {
return nil
}
info, err := s.authTokenService.GetAuthInfo(ctx, &login.GetAuthInfoQuery{AuthId: identity.GetAuthID(), UserId: userID})
if err != nil {
s.log.FromContext(ctx).Info("Failed to get auth info", "error", err, "authID", identity.GetAuthID(), "userID", userID)
return nil
}
extSession := &auth.ExternalSession{
AuthModule: identity.GetAuthenticatedBy(),
UserAuthID: info.Id,
UserID: userID,
}
extSession.AccessToken = identity.OAuthToken.AccessToken
extSession.RefreshToken = identity.OAuthToken.RefreshToken
extSession.ExpiresAt = identity.OAuthToken.Expiry
if idToken, ok := identity.OAuthToken.Extra("id_token").(string); ok && idToken != "" {
extSession.IDToken = idToken
}
// As of https://openid.net/specs/openid-connect-session-1_0.html
if sessionState, ok := identity.OAuthToken.Extra("session_state").(string); ok && sessionState != "" {
extSession.SessionID = sessionState
}
return extSession
}

View File

@ -3,7 +3,6 @@ package authnimpl
import (
"context"
"errors"
"net"
"net/http"
"net/url"
"slices"
@ -24,7 +23,6 @@ import (
"github.com/grafana/grafana/pkg/services/auth/authtest"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/authn/authntest"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
)
@ -399,11 +397,11 @@ func TestService_Login(t *testing.T) {
ExpectedIdentity: tt.expectedClientIdentity,
})
svc.sessionService = &authtest.FakeUserAuthTokenService{
CreateTokenProvider: func(ctx context.Context, user *user.User, clientIP net.IP, userAgent string) (*auth.UserToken, error) {
CreateTokenProvider: func(ctx context.Context, cmd *auth.CreateTokenCommand) (*auth.UserToken, error) {
if tt.expectedSessionErr != nil {
return nil, tt.expectedSessionErr
}
return &auth.UserToken{UserId: user.ID}, nil
return &auth.UserToken{UserId: cmd.User.ID}, nil
},
}
})

View File

@ -44,7 +44,7 @@ type Identity struct {
// IsGrafanaAdmin is true if the entity is a Grafana admin.
IsGrafanaAdmin *bool
// AuthenticatedBy is the name of the authentication client that was used to authenticate the current Identity.
// For example, "password", "apikey", "auth_ldap" or "auth_azuread".
// For example, "password", "apikey", "ldap" or "oauth_azuread".
AuthenticatedBy string
// AuthId is the unique identifier for the entity in the external system.
// Empty if the identity is provided by Grafana.

View File

@ -40,8 +40,10 @@ type LDAPMock struct {
UserSearchError error
}
var pingResult []*multildap.ServerStatus
var pingError error
var (
pingResult []*multildap.ServerStatus
pingError error
)
func (m *LDAPMock) Ping() ([]*multildap.ServerStatus, error) {
return pingResult, pingError
@ -105,7 +107,8 @@ func TestGetUserFromLDAPAPIEndpoint_UserNotFound(t *testing.T) {
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"ldap.user:read": {"*"}}},
1: {"ldap.user:read": {"*"}},
},
})
res, err := server.Send(req)
@ -170,7 +173,8 @@ func TestGetUserFromLDAPAPIEndpoint_OrgNotfound(t *testing.T) {
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"ldap.user:read": {"*"}}},
1: {"ldap.user:read": {"*"}},
},
})
res, err := server.Send(req)
@ -239,7 +243,8 @@ func TestGetUserFromLDAPAPIEndpoint(t *testing.T) {
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"ldap.user:read": {"*"}}},
1: {"ldap.user:read": {"*"}},
},
})
res, err := server.Send(req)
@ -324,7 +329,8 @@ func TestGetUserFromLDAPAPIEndpoint_WithTeamHandler(t *testing.T) {
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"ldap.user:read": {"*"}}},
1: {"ldap.user:read": {"*"}},
},
})
res, err := server.Send(req)
@ -378,7 +384,8 @@ func TestGetLDAPStatusAPIEndpoint(t *testing.T) {
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"ldap.status:read": {}}},
1: {"ldap.status:read": {}},
},
})
res, err := server.Send(req)
@ -417,7 +424,8 @@ func TestPostSyncUserWithLDAPAPIEndpoint_Success(t *testing.T) {
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"ldap.user:sync": {}}},
1: {"ldap.user:sync": {}},
},
})
res, err := server.Send(req)
@ -452,7 +460,8 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotFound(t *testing.T) {
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"ldap.user:sync": {}}},
1: {"ldap.user:sync": {}},
},
})
res, err := server.Send(req)
@ -488,7 +497,8 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenGrafanaAdmin(t *testing.T) {
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"ldap.user:sync": {}}},
1: {"ldap.user:sync": {}},
},
})
res, err := server.Send(req)
@ -521,7 +531,8 @@ func TestPostSyncUserWithLDAPAPIEndpoint_WhenUserNotInLDAP(t *testing.T) {
webtest.RequestWithSignedInUser(req, &user.SignedInUser{
OrgID: 1,
Permissions: map[int64]map[string][]string{
1: {"ldap.user:sync": {}}},
1: {"ldap.user:sync": {}},
},
})
res, err := server.Send(req)

View File

@ -18,8 +18,7 @@ import (
// initialized tracer from the opentelemetry package.
var tracer = otel.Tracer("github.com/grafana/grafana/pkg/services/quota/quotaimpl/service")
type serviceDisabled struct {
}
type serviceDisabled struct{}
func (s *serviceDisabled) QuotaReached(c *contextmodel.ReqContext, targetSrv quota.TargetSrv) (bool, error) {
return false, nil

View File

@ -483,7 +483,7 @@ func setupEnv(t *testing.T, sqlStore db.DB, cfg *setting.Cfg, b bus.Bus, quotaSe
tracer := tracing.InitializeTracerForTest()
_, err := apikeyimpl.ProvideService(sqlStore, cfg, quotaService)
require.NoError(t, err)
_, err = authimpl.ProvideUserAuthTokenService(sqlStore, nil, quotaService, cfg)
_, err = authimpl.ProvideUserAuthTokenService(sqlStore, nil, quotaService, fakes.NewFakeSecretsService(), cfg, tracing.InitializeTracerForTest())
require.NoError(t, err)
_, err = dashboardStore.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore), quotaService)
require.NoError(t, err)

View File

@ -46,6 +46,11 @@ func ProvideSecretsMigrator(
b64Secret{simpleSecret: simpleSecret{tableName: "signing_key", columnName: "private_key"}, encoding: base64.StdEncoding},
alertingSecret{},
ssoSettingsSecret{},
b64Secret{simpleSecret: simpleSecret{tableName: "user_external_session", columnName: "access_token"}, encoding: base64.StdEncoding},
b64Secret{simpleSecret: simpleSecret{tableName: "user_external_session", columnName: "id_token"}, encoding: base64.StdEncoding},
b64Secret{simpleSecret: simpleSecret{tableName: "user_external_session", columnName: "refresh_token"}, encoding: base64.StdEncoding},
b64Secret{simpleSecret: simpleSecret{tableName: "user_external_session", columnName: "session_id"}, encoding: base64.StdEncoding},
b64Secret{simpleSecret: simpleSecret{tableName: "user_external_session", columnName: "name_id"}, encoding: base64.StdEncoding},
}
return &SecretsMigrator{

View File

@ -0,0 +1,31 @@
package externalsession
import "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
func AddMigration(mg *migrator.Migrator) {
externalSessionV1 := migrator.Table{
Name: "user_external_session",
Columns: []*migrator.Column{
{Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
{Name: "user_auth_id", Type: migrator.DB_BigInt, Nullable: false},
{Name: "user_id", Type: migrator.DB_BigInt, Nullable: false},
{Name: "auth_module", Type: migrator.DB_NVarchar, Length: 190, Nullable: false},
{Name: "access_token", Type: migrator.DB_Text, Nullable: true},
{Name: "id_token", Type: migrator.DB_Text, Nullable: true},
{Name: "refresh_token", Type: migrator.DB_Text, Nullable: true},
{Name: "session_id", Type: migrator.DB_NVarchar, Length: 255, Nullable: true},
{Name: "session_id_hash", Type: migrator.DB_Char, Length: 44, Nullable: true},
{Name: "name_id", Type: migrator.DB_NVarchar, Length: 255, Nullable: true},
{Name: "name_id_hash", Type: migrator.DB_Char, Length: 44, Nullable: true},
{Name: "expires_at", Type: migrator.DB_DateTime, Nullable: true},
{Name: "created_at", Type: migrator.DB_DateTime, Nullable: false},
},
Indices: []*migrator.Index{
{Cols: []string{"user_id"}},
{Cols: []string{"session_id_hash"}},
{Cols: []string{"name_id_hash"}},
},
}
mg.AddMigration("create user_external_session table", migrator.NewAddTableMigration(externalSessionV1))
}

View File

@ -5,6 +5,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"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/externalsession"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/signingkeys"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/ssosettings"
"github.com/grafana/grafana/pkg/services/sqlstore/migrations/ualert"
@ -135,6 +136,8 @@ func (oss *OSSMigrations) AddMigration(mg *Migrator) {
accesscontrol.AddOrphanedMigrations(mg)
accesscontrol.AddActionSetPermissionsMigrator(mg)
externalsession.AddMigration(mg)
}
func addStarMigrations(mg *Migrator) {

View File

@ -48,4 +48,8 @@ func addUserAuthTokenMigrations(mg *Migrator) {
mg.AddMigration("add index user_auth_token.revoked_at", NewAddIndexMigration(userAuthTokenV1, &Index{
Cols: []string{"revoked_at"},
}))
mg.AddMigration("add external_session_id to user_auth_token", NewAddColumnMigration(userAuthTokenV1, &Column{
Name: "external_session_id", Type: DB_BigInt, Nullable: true,
}))
}

View File

@ -8366,6 +8366,10 @@
"type": "integer",
"format": "int64"
},
"ExternalSessionId": {
"type": "integer",
"format": "int64"
},
"Id": {
"type": "integer",
"format": "int64"

View File

@ -21930,6 +21930,10 @@
"type": "integer",
"format": "int64"
},
"ExternalSessionId": {
"type": "integer",
"format": "int64"
},
"Id": {
"type": "integer",
"format": "int64"

View File

@ -12158,6 +12158,10 @@
"format": "int64",
"type": "integer"
},
"ExternalSessionId": {
"format": "int64",
"type": "integer"
},
"Id": {
"format": "int64",
"type": "integer"