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

* initial from poc changes

* wip

* Remove public external session service

* Update swagger

* Fix merge

* Cleanup

* Add backgroud service for cleanup

* Add auth_module to user_external_session

* Add tests for token revocation functions

* Add secret migration capabilities for user_external_session fields

* Cleanup, refactor to address feedback

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