mirror of
https://github.com/grafana/grafana.git
synced 2025-02-16 18:34:52 -06:00
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:
parent
9eea0e99fc
commit
bd7850853e
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
244
pkg/services/auth/authimpl/external_session_store.go
Normal file
244
pkg/services/auth/authimpl/external_session_store.go
Normal 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
|
||||
}
|
228
pkg/services/auth/authimpl/external_session_store_test.go
Normal file
228
pkg/services/auth/authimpl/external_session_store_test.go
Normal 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
|
||||
}
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
162
pkg/services/auth/authtest/external_session_store_mock.go
Normal file
162
pkg/services/auth/authtest/external_session_store_mock.go
Normal 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
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
66
pkg/services/auth/external_session.go
Normal file
66
pkg/services/auth/external_session.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
},
|
||||
}
|
||||
})
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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{
|
||||
|
@ -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))
|
||||
}
|
@ -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) {
|
||||
|
@ -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,
|
||||
}))
|
||||
}
|
||||
|
@ -8366,6 +8366,10 @@
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"ExternalSessionId": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"Id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
|
@ -21930,6 +21930,10 @@
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"ExternalSessionId": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"Id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
|
@ -12158,6 +12158,10 @@
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"ExternalSessionId": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"Id": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
|
Loading…
Reference in New Issue
Block a user