mirror of https://github.com/grafana/grafana
prometheushacktoberfestmetricsmonitoringalertinggrafanagoinfluxdbmysqlpostgresanalyticsdata-visualizationdashboardbusiness-intelligenceelasticsearch
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1049 lines
30 KiB
1049 lines
30 KiB
package sync
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
claims "github.com/grafana/authlib/types"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/infra/tracing"
|
|
"github.com/grafana/grafana/pkg/services/authn"
|
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
"github.com/grafana/grafana/pkg/services/login"
|
|
"github.com/grafana/grafana/pkg/services/login/authinfoimpl"
|
|
"github.com/grafana/grafana/pkg/services/login/authinfotest"
|
|
"github.com/grafana/grafana/pkg/services/quota"
|
|
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/services/user/usertest"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
)
|
|
|
|
func ptrString(s string) *string {
|
|
return &s
|
|
}
|
|
|
|
func ptrBool(b bool) *bool {
|
|
return &b
|
|
}
|
|
|
|
func TestUserSync_SyncUserHook(t *testing.T) {
|
|
userProtection := &authinfoimpl.OSSUserProtectionImpl{}
|
|
|
|
authFakeNil := &authinfotest.FakeService{
|
|
ExpectedError: user.ErrUserNotFound,
|
|
SetAuthInfoFn: func(ctx context.Context, cmd *login.SetAuthInfoCommand) error {
|
|
return nil
|
|
},
|
|
UpdateAuthInfoFn: func(ctx context.Context, cmd *login.UpdateAuthInfoCommand) error {
|
|
return nil
|
|
},
|
|
}
|
|
authFakeUserID := &authinfotest.FakeService{
|
|
ExpectedError: nil,
|
|
ExpectedUserAuth: &login.UserAuth{
|
|
AuthModule: "oauth",
|
|
AuthId: "2032",
|
|
UserId: 1,
|
|
Id: 1,
|
|
},
|
|
}
|
|
|
|
userService := &usertest.FakeUserService{ExpectedUser: &user.User{
|
|
ID: 1,
|
|
UID: "1",
|
|
Login: "test",
|
|
Name: "test",
|
|
Email: "test",
|
|
}}
|
|
|
|
userServiceMod := &usertest.FakeUserService{ExpectedUser: &user.User{
|
|
ID: 3,
|
|
UID: "3",
|
|
Login: "test",
|
|
Name: "test",
|
|
Email: "test",
|
|
IsDisabled: true,
|
|
IsAdmin: false,
|
|
}}
|
|
|
|
userServiceEmailMod := &usertest.FakeUserService{ExpectedUser: &user.User{
|
|
ID: 3,
|
|
UID: "3",
|
|
Login: "test",
|
|
Name: "test",
|
|
Email: "test@test.com",
|
|
EmailVerified: true,
|
|
IsDisabled: true,
|
|
IsAdmin: false,
|
|
}}
|
|
|
|
userServiceNil := &usertest.FakeUserService{
|
|
ExpectedError: user.ErrUserNotFound,
|
|
CreateFn: func(ctx context.Context, cmd *user.CreateUserCommand) (*user.User, error) {
|
|
return &user.User{
|
|
ID: 2,
|
|
UID: "2",
|
|
Login: cmd.Login,
|
|
Name: cmd.Name,
|
|
Email: cmd.Email,
|
|
IsAdmin: cmd.IsAdmin,
|
|
}, nil
|
|
},
|
|
}
|
|
|
|
type fields struct {
|
|
userService user.Service
|
|
authInfoService login.AuthInfoService
|
|
quotaService quota.Service
|
|
}
|
|
type args struct {
|
|
ctx context.Context
|
|
id *authn.Identity
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
fields fields
|
|
args args
|
|
wantErr bool
|
|
wantID *authn.Identity
|
|
}{
|
|
{
|
|
name: "no sync",
|
|
fields: fields{
|
|
userService: userService,
|
|
authInfoService: authFakeNil,
|
|
quotaService: "atest.FakeQuotaService{},
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
id: &authn.Identity{
|
|
Login: "test",
|
|
Name: "test",
|
|
Email: "test",
|
|
ClientParams: authn.ClientParams{
|
|
LookUpParams: login.UserLookupParams{
|
|
Email: ptrString("test"),
|
|
Login: nil,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
wantID: &authn.Identity{
|
|
Login: "test",
|
|
Name: "test",
|
|
Email: "test",
|
|
ClientParams: authn.ClientParams{
|
|
LookUpParams: login.UserLookupParams{
|
|
Email: ptrString("test"),
|
|
Login: nil,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "sync - user found in DB - by email",
|
|
fields: fields{
|
|
userService: userService,
|
|
authInfoService: authFakeNil,
|
|
quotaService: "atest.FakeQuotaService{},
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
id: &authn.Identity{
|
|
Login: "test",
|
|
Name: "test",
|
|
Email: "test",
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
LookUpParams: login.UserLookupParams{
|
|
Email: ptrString("test"),
|
|
Login: nil,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
wantID: &authn.Identity{
|
|
ID: "1",
|
|
UID: "1",
|
|
Type: claims.TypeUser,
|
|
Login: "test",
|
|
Name: "test",
|
|
Email: "test",
|
|
IsGrafanaAdmin: ptrBool(false),
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
LookUpParams: login.UserLookupParams{
|
|
Email: ptrString("test"),
|
|
Login: nil,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "sync - user found in DB - by login",
|
|
fields: fields{
|
|
userService: userService,
|
|
authInfoService: authFakeNil,
|
|
quotaService: "atest.FakeQuotaService{},
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
id: &authn.Identity{
|
|
Login: "test",
|
|
Name: "test",
|
|
Email: "test",
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
LookUpParams: login.UserLookupParams{
|
|
Email: nil,
|
|
Login: ptrString("test"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
wantID: &authn.Identity{
|
|
ID: "1",
|
|
UID: "1",
|
|
Type: claims.TypeUser,
|
|
Login: "test",
|
|
Name: "test",
|
|
Email: "test",
|
|
IsGrafanaAdmin: ptrBool(false),
|
|
ClientParams: authn.ClientParams{
|
|
LookUpParams: login.UserLookupParams{
|
|
Email: nil,
|
|
Login: ptrString("test"),
|
|
},
|
|
SyncUser: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "sync - user found in authInfo",
|
|
fields: fields{
|
|
userService: userService,
|
|
authInfoService: authFakeUserID,
|
|
quotaService: "atest.FakeQuotaService{},
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
id: &authn.Identity{
|
|
AuthID: "2032",
|
|
AuthenticatedBy: "oauth",
|
|
Login: "test",
|
|
Name: "test",
|
|
Email: "test",
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
LookUpParams: login.UserLookupParams{
|
|
Email: nil,
|
|
Login: nil,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
wantID: &authn.Identity{
|
|
ID: "1",
|
|
UID: "1",
|
|
Type: claims.TypeUser,
|
|
AuthID: "2032",
|
|
AuthenticatedBy: "oauth",
|
|
Login: "test",
|
|
Name: "test",
|
|
Email: "test",
|
|
IsGrafanaAdmin: ptrBool(false),
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
LookUpParams: login.UserLookupParams{
|
|
Email: nil,
|
|
Login: nil,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "sync - user needs to be created - disabled signup",
|
|
fields: fields{
|
|
userService: userService,
|
|
authInfoService: authFakeNil,
|
|
quotaService: "atest.FakeQuotaService{},
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
id: &authn.Identity{
|
|
Login: "test",
|
|
Name: "test",
|
|
Email: "test",
|
|
AuthenticatedBy: "oauth",
|
|
AuthID: "2032",
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
LookUpParams: login.UserLookupParams{
|
|
Email: nil,
|
|
Login: nil,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
wantErr: true,
|
|
},
|
|
{
|
|
name: "sync - user needs to be created - enabled signup",
|
|
fields: fields{
|
|
userService: userServiceNil,
|
|
authInfoService: authFakeNil,
|
|
quotaService: "atest.FakeQuotaService{},
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
id: &authn.Identity{
|
|
Login: "test_create",
|
|
Name: "test_create",
|
|
IsGrafanaAdmin: ptrBool(true),
|
|
Email: "test_create",
|
|
AuthenticatedBy: "oauth",
|
|
AuthID: "2032",
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
AllowSignUp: true,
|
|
EnableUser: true,
|
|
LookUpParams: login.UserLookupParams{
|
|
Email: ptrString("test_create"),
|
|
Login: nil,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
wantID: &authn.Identity{
|
|
ID: "2",
|
|
UID: "2",
|
|
Type: claims.TypeUser,
|
|
Login: "test_create",
|
|
Name: "test_create",
|
|
Email: "test_create",
|
|
AuthenticatedBy: "oauth",
|
|
AuthID: "2032",
|
|
IsGrafanaAdmin: ptrBool(true),
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
AllowSignUp: true,
|
|
EnableUser: true,
|
|
LookUpParams: login.UserLookupParams{
|
|
Email: ptrString("test_create"),
|
|
Login: nil,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "sync - needs full update",
|
|
fields: fields{
|
|
userService: userServiceMod,
|
|
authInfoService: authFakeNil,
|
|
quotaService: "atest.FakeQuotaService{},
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
id: &authn.Identity{
|
|
Login: "test_mod",
|
|
Name: "test_mod",
|
|
Email: "test_mod",
|
|
IsDisabled: false,
|
|
IsGrafanaAdmin: ptrBool(true),
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
EnableUser: true,
|
|
LookUpParams: login.UserLookupParams{
|
|
Email: nil,
|
|
Login: ptrString("test"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
wantID: &authn.Identity{
|
|
ID: "3",
|
|
UID: "3",
|
|
Type: claims.TypeUser,
|
|
Login: "test_mod",
|
|
Name: "test_mod",
|
|
Email: "test_mod",
|
|
IsDisabled: false,
|
|
IsGrafanaAdmin: ptrBool(true),
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
EnableUser: true,
|
|
LookUpParams: login.UserLookupParams{
|
|
Email: nil,
|
|
Login: ptrString("test"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "sync - reset email verified on email change",
|
|
fields: fields{
|
|
userService: userServiceEmailMod,
|
|
authInfoService: authFakeNil,
|
|
quotaService: "atest.FakeQuotaService{},
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
id: &authn.Identity{
|
|
Login: "test",
|
|
Name: "test",
|
|
Email: "test_mod@test.com",
|
|
EmailVerified: true,
|
|
IsDisabled: false,
|
|
IsGrafanaAdmin: ptrBool(true),
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
EnableUser: true,
|
|
LookUpParams: login.UserLookupParams{
|
|
Email: nil,
|
|
Login: ptrString("test"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
wantID: &authn.Identity{
|
|
ID: "3",
|
|
UID: "3",
|
|
Type: claims.TypeUser,
|
|
Name: "test",
|
|
Login: "test",
|
|
Email: "test_mod@test.com",
|
|
IsDisabled: false,
|
|
EmailVerified: false,
|
|
IsGrafanaAdmin: ptrBool(true),
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
EnableUser: true,
|
|
LookUpParams: login.UserLookupParams{
|
|
Email: nil,
|
|
Login: ptrString("test"),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "SyncUserHook: Provisioned user, Incoming ExternalUID is empty, DB ExternalUID non-empty - expect errEmptyExternalUID",
|
|
fields: fields{
|
|
userService: &usertest.FakeUserService{ExpectedUser: &user.User{ID: 1, IsProvisioned: true}},
|
|
authInfoService: &authinfotest.FakeService{ExpectedUserAuth: &login.UserAuth{UserId: 1, AuthModule: login.SAMLAuthModule, ExternalUID: "db-uid"}},
|
|
quotaService: "atest.FakeQuotaService{},
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
id: &authn.Identity{
|
|
AuthID: "1",
|
|
AuthenticatedBy: login.SAMLAuthModule,
|
|
ExternalUID: "",
|
|
ClientParams: authn.ClientParams{SyncUser: true},
|
|
},
|
|
},
|
|
wantErr: true, // Expecting errEmptyExternalUID
|
|
},
|
|
{
|
|
name: "SyncUserHook: Provisioned user, Incoming ExternalUID is empty, DB ExternalUID also empty - expect errEmptyExternalUID",
|
|
fields: fields{
|
|
userService: &usertest.FakeUserService{ExpectedUser: &user.User{ID: 1, IsProvisioned: true}},
|
|
authInfoService: &authinfotest.FakeService{ExpectedUserAuth: &login.UserAuth{UserId: 1, AuthModule: login.SAMLAuthModule, ExternalUID: ""}}, // DB empty
|
|
quotaService: "atest.FakeQuotaService{},
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
id: &authn.Identity{
|
|
AuthID: "1",
|
|
AuthenticatedBy: login.SAMLAuthModule,
|
|
ExternalUID: "",
|
|
ClientParams: authn.ClientParams{SyncUser: true},
|
|
},
|
|
},
|
|
wantErr: true, // Expecting errEmptyExternalUID
|
|
},
|
|
{
|
|
name: "SyncUserHook: Provisioned user, Incoming and DB ExternalUIDs non-empty and mismatch - expect errMismatchedExternalUID",
|
|
fields: fields{
|
|
userService: &usertest.FakeUserService{ExpectedUser: &user.User{ID: 1, IsProvisioned: true}},
|
|
authInfoService: &authinfotest.FakeService{ExpectedUserAuth: &login.UserAuth{UserId: 1, AuthModule: login.SAMLAuthModule, ExternalUID: "db-uid"}},
|
|
quotaService: "atest.FakeQuotaService{},
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
id: &authn.Identity{
|
|
AuthID: "1",
|
|
AuthenticatedBy: login.SAMLAuthModule,
|
|
ExternalUID: "incoming-uid",
|
|
ClientParams: authn.ClientParams{SyncUser: true},
|
|
},
|
|
},
|
|
wantErr: true, // Expecting errMismatchedExternalUID
|
|
},
|
|
{
|
|
name: "SyncUserHook: Provisioned user, Incoming and DB ExternalUIDs non-empty and match - expect success",
|
|
fields: fields{
|
|
userService: &usertest.FakeUserService{ExpectedUser: &user.User{ID: 1, Login: "user1", Email: "user1@test.com", Name: "User One", IsProvisioned: true}},
|
|
authInfoService: &authinfotest.FakeService{ExpectedUserAuth: &login.UserAuth{UserId: 1, AuthModule: login.SAMLAuthModule, AuthId: "1", ExternalUID: "matching-uid"}},
|
|
quotaService: "atest.FakeQuotaService{},
|
|
},
|
|
args: args{
|
|
ctx: context.Background(),
|
|
id: &authn.Identity{
|
|
AuthID: "1",
|
|
AuthenticatedBy: login.SAMLAuthModule,
|
|
Login: "user1",
|
|
Email: "user1@test.com",
|
|
Name: "User One",
|
|
ExternalUID: "matching-uid",
|
|
ClientParams: authn.ClientParams{SyncUser: true},
|
|
},
|
|
},
|
|
wantErr: false,
|
|
wantID: &authn.Identity{
|
|
ID: "1",
|
|
UID: "",
|
|
Type: claims.TypeUser,
|
|
AuthID: "1",
|
|
AuthenticatedBy: login.SAMLAuthModule,
|
|
Login: "user1",
|
|
Email: "user1@test.com",
|
|
Name: "User One",
|
|
ExternalUID: "matching-uid",
|
|
IsGrafanaAdmin: ptrBool(false),
|
|
ClientParams: authn.ClientParams{SyncUser: true},
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
s := ProvideUserSync(tt.fields.userService, userProtection, tt.fields.authInfoService, tt.fields.quotaService, tracing.InitializeTracerForTest(), featuremgmt.WithFeatures(), setting.NewCfg())
|
|
err := s.SyncUserHook(tt.args.ctx, tt.args.id, nil)
|
|
if tt.wantErr {
|
|
require.Error(t, err)
|
|
return
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
require.EqualValues(t, tt.wantID, tt.args.id)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUserSync_SyncUserRetryFetch(t *testing.T) {
|
|
userSrv := usertest.NewMockService(t)
|
|
userSrv.On("GetByEmail", mock.Anything, mock.Anything).Return(nil, user.ErrUserNotFound).Once()
|
|
userSrv.On("Create", mock.Anything, mock.Anything).Return(nil, user.ErrUserAlreadyExists).Once()
|
|
userSrv.On("GetByEmail", mock.Anything, mock.Anything).Return(&user.User{ID: 1}, nil).Once()
|
|
|
|
s := ProvideUserSync(
|
|
userSrv,
|
|
authinfoimpl.ProvideOSSUserProtectionService(),
|
|
&authinfotest.FakeService{},
|
|
"atest.FakeQuotaService{},
|
|
tracing.NewNoopTracerService(),
|
|
featuremgmt.WithFeatures(),
|
|
setting.NewCfg(),
|
|
)
|
|
|
|
email := "test@test.com"
|
|
|
|
err := s.SyncUserHook(context.Background(), &authn.Identity{
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
AllowSignUp: true,
|
|
LookUpParams: login.UserLookupParams{
|
|
Email: &email,
|
|
},
|
|
},
|
|
}, nil)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestUserSync_FetchSyncedUserHook(t *testing.T) {
|
|
type testCase struct {
|
|
desc string
|
|
req *authn.Request
|
|
identity *authn.Identity
|
|
expectedErr error
|
|
}
|
|
|
|
tests := []testCase{
|
|
{
|
|
desc: "should skip hook when flag is not enabled",
|
|
req: &authn.Request{},
|
|
identity: &authn.Identity{ClientParams: authn.ClientParams{FetchSyncedUser: false}},
|
|
},
|
|
{
|
|
desc: "should skip hook when identity is not a user",
|
|
req: &authn.Request{},
|
|
identity: &authn.Identity{ID: "1", Type: claims.TypeAPIKey, ClientParams: authn.ClientParams{FetchSyncedUser: true}},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
s := UserSync{
|
|
tracer: tracing.InitializeTracerForTest(),
|
|
}
|
|
err := s.FetchSyncedUserHook(context.Background(), tt.identity, tt.req)
|
|
require.ErrorIs(t, err, tt.expectedErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUserSync_EnableDisabledUserHook(t *testing.T) {
|
|
type testCase struct {
|
|
desc string
|
|
identity *authn.Identity
|
|
enableUser bool
|
|
}
|
|
|
|
tests := []testCase{
|
|
{
|
|
desc: "should skip if correct flag is not set",
|
|
identity: &authn.Identity{
|
|
ID: "1",
|
|
Type: claims.TypeUser,
|
|
IsDisabled: true,
|
|
ClientParams: authn.ClientParams{EnableUser: false},
|
|
},
|
|
enableUser: false,
|
|
},
|
|
{
|
|
desc: "should skip if identity is not a user",
|
|
identity: &authn.Identity{
|
|
ID: "1",
|
|
Type: claims.TypeAPIKey,
|
|
IsDisabled: true,
|
|
ClientParams: authn.ClientParams{EnableUser: true},
|
|
},
|
|
enableUser: false,
|
|
},
|
|
{
|
|
desc: "should enabled disabled user",
|
|
identity: &authn.Identity{
|
|
ID: "1",
|
|
Type: claims.TypeUser,
|
|
IsDisabled: true,
|
|
ClientParams: authn.ClientParams{EnableUser: true},
|
|
},
|
|
enableUser: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
userSvc := usertest.NewUserServiceFake()
|
|
called := false
|
|
userSvc.UpdateFn = func(ctx context.Context, cmd *user.UpdateUserCommand) error {
|
|
called = true
|
|
return nil
|
|
}
|
|
|
|
s := UserSync{userService: userSvc, tracer: tracing.InitializeTracerForTest()}
|
|
err := s.EnableUserHook(context.Background(), tt.identity, nil)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.enableUser, called)
|
|
})
|
|
}
|
|
}
|
|
|
|
func initUserSyncService() *UserSync {
|
|
userSvc := usertest.NewUserServiceFake()
|
|
log := log.New("test")
|
|
authInfoSvc := &authinfotest.FakeService{
|
|
ExpectedUserAuth: &login.UserAuth{
|
|
UserId: 1,
|
|
AuthModule: login.SAMLAuthModule,
|
|
AuthId: "1",
|
|
},
|
|
}
|
|
quotaSvc := "atest.FakeQuotaService{}
|
|
return &UserSync{
|
|
userService: userSvc,
|
|
authInfoService: authInfoSvc,
|
|
quotaService: quotaSvc,
|
|
tracer: tracing.InitializeTracerForTest(),
|
|
log: log,
|
|
}
|
|
}
|
|
|
|
func TestUserSync_ValidateUserProvisioningHook(t *testing.T) {
|
|
type testCase struct {
|
|
desc string
|
|
identity *authn.Identity
|
|
userSyncServiceSetup func() *UserSync
|
|
expectedErr error
|
|
}
|
|
|
|
tests := []testCase{
|
|
{
|
|
desc: "it should skip validation if the user identity is not syncying a user",
|
|
userSyncServiceSetup: func() *UserSync {
|
|
userSyncService := initUserSyncService()
|
|
userSyncService.isUserProvisioningEnabled = true
|
|
return userSyncService
|
|
},
|
|
identity: &authn.Identity{
|
|
ID: "1",
|
|
Type: claims.TypeAPIKey,
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: false,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
desc: "it should skip validation if the user provisioning is disabled",
|
|
userSyncServiceSetup: func() *UserSync {
|
|
userSyncService := initUserSyncService()
|
|
userSyncService.isUserProvisioningEnabled = false
|
|
return userSyncService
|
|
},
|
|
identity: &authn.Identity{
|
|
AuthenticatedBy: login.GenericOAuthModule,
|
|
AuthID: "1",
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
desc: "it should skip validation if allowedNonProvisionedUsers is enabled",
|
|
userSyncServiceSetup: func() *UserSync {
|
|
userSyncService := initUserSyncService()
|
|
userSyncService.allowNonProvisionedUsers = true
|
|
userSyncService.isUserProvisioningEnabled = true
|
|
return userSyncService
|
|
},
|
|
identity: &authn.Identity{
|
|
AuthenticatedBy: login.GenericOAuthModule,
|
|
AuthID: "1",
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
desc: "it should skip validation if the user is authenticated via GrafanaComAuthModule",
|
|
userSyncServiceSetup: func() *UserSync {
|
|
userSyncService := initUserSyncService()
|
|
userSyncService.allowNonProvisionedUsers = false
|
|
userSyncService.isUserProvisioningEnabled = true
|
|
return userSyncService
|
|
},
|
|
identity: &authn.Identity{
|
|
AuthenticatedBy: login.GrafanaComAuthModule,
|
|
AuthID: "1",
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
desc: "it should fail to validate the identity with the provisioned user, unexpected error",
|
|
userSyncServiceSetup: func() *UserSync {
|
|
userSyncService := initUserSyncService()
|
|
userSyncService.allowNonProvisionedUsers = false
|
|
userSyncService.isUserProvisioningEnabled = true
|
|
userSyncService.userService = &usertest.FakeUserService{
|
|
ExpectedError: errors.New("random error"),
|
|
}
|
|
return userSyncService
|
|
},
|
|
identity: &authn.Identity{
|
|
AuthenticatedBy: login.SAMLAuthModule,
|
|
AuthID: "1",
|
|
ExternalUID: "random-external-uid",
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
},
|
|
},
|
|
expectedErr: errUnableToRetrieveUserOrAuthInfo.Errorf("unable to retrieve user or authInfo for validation"),
|
|
},
|
|
{
|
|
desc: "it should fail to validate the identity with the provisioned user, no user found",
|
|
userSyncServiceSetup: func() *UserSync {
|
|
userSyncService := initUserSyncService()
|
|
userSyncService.allowNonProvisionedUsers = false
|
|
userSyncService.isUserProvisioningEnabled = true
|
|
userSyncService.userService = &usertest.FakeUserService{}
|
|
return userSyncService
|
|
},
|
|
identity: &authn.Identity{
|
|
AuthenticatedBy: login.SAMLAuthModule,
|
|
AuthID: "1",
|
|
ExternalUID: "random-external-uid",
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
},
|
|
},
|
|
expectedErr: errUnableToRetrieveUser.Errorf("unable to retrieve user for validation"),
|
|
},
|
|
{
|
|
desc: "it should fail to validate the provisioned user.ExternalUID with the identity.ExternalUID - empty ExternalUID",
|
|
userSyncServiceSetup: func() *UserSync {
|
|
userSyncService := initUserSyncService()
|
|
userSyncService.allowNonProvisionedUsers = false
|
|
userSyncService.isUserProvisioningEnabled = true
|
|
userSyncService.userService = &usertest.FakeUserService{
|
|
ExpectedUser: &user.User{
|
|
ID: 1,
|
|
IsProvisioned: true,
|
|
},
|
|
}
|
|
userSyncService.authInfoService = &authinfotest.FakeService{
|
|
ExpectedUserAuth: &login.UserAuth{
|
|
UserId: 1,
|
|
AuthModule: login.SAMLAuthModule,
|
|
AuthId: "1",
|
|
},
|
|
}
|
|
return userSyncService
|
|
},
|
|
identity: &authn.Identity{
|
|
AuthenticatedBy: login.SAMLAuthModule,
|
|
AuthID: "1",
|
|
ExternalUID: "random-external-uid",
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
},
|
|
},
|
|
expectedErr: errUserExternalUIDMismatch.Errorf("the provisioned user.ExternalUID does not match the authinfo.ExternalUID"),
|
|
},
|
|
{
|
|
desc: "it should fail to validate the provisioned user.ExternalUID with the identity.ExternalUID - different ExternalUID",
|
|
userSyncServiceSetup: func() *UserSync {
|
|
userSyncService := initUserSyncService()
|
|
userSyncService.allowNonProvisionedUsers = false
|
|
userSyncService.isUserProvisioningEnabled = true
|
|
userSyncService.userService = &usertest.FakeUserService{
|
|
ExpectedUser: &user.User{
|
|
ID: 1,
|
|
IsProvisioned: true,
|
|
},
|
|
}
|
|
userSyncService.authInfoService = &authinfotest.FakeService{
|
|
ExpectedUserAuth: &login.UserAuth{
|
|
UserId: 1,
|
|
AuthModule: login.SAMLAuthModule,
|
|
AuthId: "1",
|
|
ExternalUID: "different-external-uid",
|
|
},
|
|
}
|
|
return userSyncService
|
|
},
|
|
identity: &authn.Identity{
|
|
AuthenticatedBy: login.SAMLAuthModule,
|
|
AuthID: "1",
|
|
ExternalUID: "random-external-uid",
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
},
|
|
},
|
|
expectedErr: errUserExternalUIDMismatch.Errorf("the provisioned user.ExternalUID does not match the authinfo.ExternalUID"),
|
|
},
|
|
{
|
|
desc: "it should successfully validate the provisioned user.ExternalUID with the identity.ExternalUID",
|
|
userSyncServiceSetup: func() *UserSync {
|
|
userSyncService := initUserSyncService()
|
|
userSyncService.allowNonProvisionedUsers = false
|
|
userSyncService.isUserProvisioningEnabled = true
|
|
userSyncService.userService = &usertest.FakeUserService{
|
|
ExpectedUser: &user.User{
|
|
ID: 1,
|
|
IsProvisioned: true,
|
|
},
|
|
}
|
|
userSyncService.authInfoService = &authinfotest.FakeService{
|
|
ExpectedUserAuth: &login.UserAuth{
|
|
UserId: 1,
|
|
AuthModule: login.SAMLAuthModule,
|
|
AuthId: "1",
|
|
ExternalUID: "random-external-uid",
|
|
},
|
|
}
|
|
return userSyncService
|
|
},
|
|
identity: &authn.Identity{
|
|
AuthenticatedBy: login.SAMLAuthModule,
|
|
AuthID: "1",
|
|
ExternalUID: "random-external-uid",
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
desc: "it should failed to validate a non provisioned user when retrieved from the database",
|
|
userSyncServiceSetup: func() *UserSync {
|
|
userSyncService := initUserSyncService()
|
|
userSyncService.allowNonProvisionedUsers = false
|
|
userSyncService.isUserProvisioningEnabled = true
|
|
userSyncService.userService = &usertest.FakeUserService{
|
|
ExpectedUser: &user.User{
|
|
ID: 1,
|
|
IsProvisioned: false,
|
|
},
|
|
}
|
|
userSyncService.authInfoService = &authinfotest.FakeService{
|
|
ExpectedUserAuth: &login.UserAuth{
|
|
UserId: 1,
|
|
AuthModule: login.SAMLAuthModule,
|
|
AuthId: "1",
|
|
ExternalUID: "random-external-uid",
|
|
},
|
|
}
|
|
return userSyncService
|
|
},
|
|
identity: &authn.Identity{
|
|
AuthenticatedBy: login.SAMLAuthModule,
|
|
AuthID: "1",
|
|
ExternalUID: "random-external-uid",
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
},
|
|
},
|
|
expectedErr: errUserNotProvisioned.Errorf("user is not provisioned"),
|
|
},
|
|
{
|
|
desc: "ValidateProvisioning: DB ExternalUID is empty, Incoming ExternalUID is empty - expect mismatch (stricter logic)",
|
|
userSyncServiceSetup: func() *UserSync {
|
|
userSyncService := initUserSyncService()
|
|
userSyncService.isUserProvisioningEnabled = true
|
|
userSyncService.userService = &usertest.FakeUserService{ExpectedUser: &user.User{ID: 1, IsProvisioned: true}}
|
|
userSyncService.authInfoService = &authinfotest.FakeService{ExpectedUserAuth: &login.UserAuth{UserId: 1, AuthModule: login.SAMLAuthModule, ExternalUID: ""}}
|
|
return userSyncService
|
|
},
|
|
identity: &authn.Identity{
|
|
AuthenticatedBy: login.SAMLAuthModule,
|
|
AuthID: "1",
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
},
|
|
ExternalUID: "",
|
|
},
|
|
expectedErr: errUserExternalUIDMismatch,
|
|
},
|
|
{
|
|
desc: "ValidateProvisioning: DB ExternalUID is empty, Incoming ExternalUID non-empty - expect mismatch (stricter logic)",
|
|
userSyncServiceSetup: func() *UserSync {
|
|
userSyncService := initUserSyncService()
|
|
userSyncService.isUserProvisioningEnabled = true
|
|
userSyncService.userService = &usertest.FakeUserService{ExpectedUser: &user.User{ID: 1, IsProvisioned: true}}
|
|
userSyncService.authInfoService = &authinfotest.FakeService{ExpectedUserAuth: &login.UserAuth{UserId: 1, AuthModule: login.SAMLAuthModule, ExternalUID: ""}}
|
|
return userSyncService
|
|
},
|
|
identity: &authn.Identity{
|
|
AuthenticatedBy: login.SAMLAuthModule,
|
|
AuthID: "1",
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
},
|
|
ExternalUID: "valid-uid",
|
|
},
|
|
expectedErr: errUserExternalUIDMismatch,
|
|
},
|
|
{
|
|
desc: "ValidateProvisioning: DB and Incoming ExternalUIDs non-empty and mismatch - expect mismatch",
|
|
userSyncServiceSetup: func() *UserSync {
|
|
userSyncService := initUserSyncService()
|
|
userSyncService.isUserProvisioningEnabled = true
|
|
userSyncService.userService = &usertest.FakeUserService{ExpectedUser: &user.User{ID: 1, IsProvisioned: true}}
|
|
userSyncService.authInfoService = &authinfotest.FakeService{ExpectedUserAuth: &login.UserAuth{UserId: 1, AuthModule: login.SAMLAuthModule, ExternalUID: "db-uid"}}
|
|
return userSyncService
|
|
},
|
|
identity: &authn.Identity{
|
|
AuthenticatedBy: login.SAMLAuthModule,
|
|
AuthID: "1",
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
},
|
|
ExternalUID: "incoming-uid",
|
|
},
|
|
expectedErr: errUserExternalUIDMismatch,
|
|
},
|
|
{
|
|
desc: "it should skip ExternalUID validation for a SAML-provisioned user accessed by a non-SAML method with an empty incoming ExternalUID",
|
|
userSyncServiceSetup: func() *UserSync {
|
|
userSyncService := initUserSyncService()
|
|
userSyncService.allowNonProvisionedUsers = false
|
|
userSyncService.isUserProvisioningEnabled = true
|
|
userSyncService.userService = &usertest.FakeUserService{
|
|
ExpectedUser: &user.User{
|
|
ID: 1,
|
|
IsProvisioned: true,
|
|
},
|
|
}
|
|
userSyncService.authInfoService = &authinfotest.FakeService{
|
|
ExpectedUserAuth: &login.UserAuth{
|
|
UserId: 1,
|
|
AuthModule: login.SAMLAuthModule,
|
|
AuthId: "1",
|
|
ExternalUID: "saml-originated-uid",
|
|
},
|
|
}
|
|
return userSyncService
|
|
},
|
|
identity: &authn.Identity{
|
|
AuthenticatedBy: login.GenericOAuthModule,
|
|
AuthID: "1",
|
|
ExternalUID: "",
|
|
},
|
|
expectedErr: nil,
|
|
},
|
|
{
|
|
desc: "it should fail validation when a provisioned user is accessed by SAML with an empty incoming ExternalUID",
|
|
userSyncServiceSetup: func() *UserSync {
|
|
userSyncService := initUserSyncService()
|
|
userSyncService.allowNonProvisionedUsers = false
|
|
userSyncService.isUserProvisioningEnabled = true
|
|
userSyncService.userService = &usertest.FakeUserService{
|
|
ExpectedUser: &user.User{
|
|
ID: 1,
|
|
IsProvisioned: true,
|
|
},
|
|
}
|
|
userSyncService.authInfoService = &authinfotest.FakeService{
|
|
ExpectedUserAuth: &login.UserAuth{
|
|
UserId: 1,
|
|
AuthModule: login.SAMLAuthModule,
|
|
AuthId: "1",
|
|
ExternalUID: "saml-originated-uid",
|
|
},
|
|
}
|
|
return userSyncService
|
|
},
|
|
identity: &authn.Identity{
|
|
AuthenticatedBy: login.SAMLAuthModule,
|
|
AuthID: "1",
|
|
ExternalUID: "",
|
|
ClientParams: authn.ClientParams{
|
|
SyncUser: true,
|
|
},
|
|
},
|
|
expectedErr: errUserExternalUIDMismatch.Errorf("the provisioned user.ExternalUID does not match the authinfo.ExternalUID"),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.desc, func(t *testing.T) {
|
|
userSyncService := tt.userSyncServiceSetup()
|
|
err := userSyncService.ValidateUserProvisioningHook(context.Background(), tt.identity, nil)
|
|
require.ErrorIs(t, err, tt.expectedErr)
|
|
})
|
|
}
|
|
}
|
|
|