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:
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user