/
user_sync.go
423 lines (363 loc) · 13.3 KB
/
user_sync.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
package sync
import (
"context"
"errors"
"fmt"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util/errutil"
)
var (
errUserSignupDisabled = errutil.Unauthorized(
"user.sync.signup-disabled",
errutil.WithPublicMessage("Sign up is disabled"),
)
errSyncUserForbidden = errutil.Forbidden(
"user.sync.forbidden",
errutil.WithPublicMessage("User sync forbidden"),
)
errSyncUserInternal = errutil.Internal(
"user.sync.internal",
errutil.WithPublicMessage("User sync failed"),
)
errUserProtection = errutil.Forbidden(
"user.sync.protected-role",
errutil.WithPublicMessage("Unable to sync due to protected role"),
)
errFetchingSignedInUser = errutil.Internal(
"user.sync.fetch",
errutil.WithPublicMessage("Insufficient information to authenticate user"),
)
errFetchingSignedInUserNotFound = errutil.Unauthorized(
"user.sync.fetch-not-found",
errutil.WithPublicMessage("User not found"),
)
)
var (
errUsersQuotaReached = errors.New("users quota reached")
errGettingUserQuota = errors.New("error getting user quota")
errSignupNotAllowed = errors.New("system administrator has disabled signup")
)
func ProvideUserSync(userService user.Service,
userProtectionService login.UserProtectionService,
authInfoService login.AuthInfoService, quotaService quota.Service) *UserSync {
return &UserSync{
userService: userService,
authInfoService: authInfoService,
userProtectionService: userProtectionService,
quotaService: quotaService,
log: log.New("user.sync"),
}
}
type UserSync struct {
userService user.Service
authInfoService login.AuthInfoService
userProtectionService login.UserProtectionService
quotaService quota.Service
log log.Logger
}
// SyncUserHook syncs a user with the database
func (s *UserSync) SyncUserHook(ctx context.Context, id *authn.Identity, _ *authn.Request) error {
if !id.ClientParams.SyncUser {
return nil
}
// Does user exist in the database?
usr, userAuth, errUserInDB := s.getUser(ctx, id)
if errUserInDB != nil && !errors.Is(errUserInDB, user.ErrUserNotFound) {
s.log.FromContext(ctx).Error("Failed to fetch user", "error", errUserInDB, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID)
return errSyncUserInternal.Errorf("unable to retrieve user")
}
if errors.Is(errUserInDB, user.ErrUserNotFound) {
if !id.ClientParams.AllowSignUp {
s.log.FromContext(ctx).Warn("Failed to create user, signup is not allowed for module", "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID)
return errUserSignupDisabled.Errorf("%w", errSignupNotAllowed)
}
// create user
var errCreate error
usr, errCreate = s.createUser(ctx, id)
if errCreate != nil {
s.log.FromContext(ctx).Error("Failed to create user", "error", errCreate, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID)
return errSyncUserInternal.Errorf("unable to create user: %w", errCreate)
}
} else {
// update user
if errUpdate := s.updateUserAttributes(ctx, usr, id, userAuth); errUpdate != nil {
s.log.FromContext(ctx).Error("Failed to update user", "error", errUpdate, "auth_module", id.AuthenticatedBy, "auth_id", id.AuthID)
return errSyncUserInternal.Errorf("unable to update user")
}
}
syncUserToIdentity(usr, id)
return nil
}
func (s *UserSync) FetchSyncedUserHook(ctx context.Context, identity *authn.Identity, r *authn.Request) error {
if !identity.ClientParams.FetchSyncedUser {
return nil
}
if !identity.ID.IsNamespace(authn.NamespaceUser, authn.NamespaceServiceAccount) {
return nil
}
userID, err := identity.ID.ParseInt()
if err != nil {
s.log.FromContext(ctx).Warn("got invalid identity ID", "id", identity.ID, "err", err)
return nil
}
usr, err := s.userService.GetSignedInUser(ctx, &user.GetSignedInUserQuery{
UserID: userID,
OrgID: r.OrgID,
})
if err != nil {
if errors.Is(err, user.ErrUserNotFound) {
return errFetchingSignedInUserNotFound.Errorf("%w", err)
}
return errFetchingSignedInUser.Errorf("failed to resolve user: %w", err)
}
if identity.ClientParams.AllowGlobalOrg && identity.OrgID == authn.GlobalOrgID {
usr.Teams = nil
usr.OrgName = ""
usr.OrgRole = org.RoleNone
usr.OrgID = authn.GlobalOrgID
}
syncSignedInUserToIdentity(usr, identity)
return nil
}
func (s *UserSync) SyncLastSeenHook(ctx context.Context, identity *authn.Identity, r *authn.Request) error {
if r.GetMeta(authn.MetaKeyIsLogin) != "" {
// Do not sync last seen for login requests
return nil
}
if !identity.ID.IsNamespace(authn.NamespaceUser, authn.NamespaceServiceAccount) {
return nil
}
userID, err := identity.ID.ParseInt()
if err != nil {
s.log.FromContext(ctx).Warn("got invalid identity ID", "id", identity.ID, "err", err)
return nil
}
go func(userID int64) {
defer func() {
if err := recover(); err != nil {
s.log.Error("Panic during user last seen sync", "err", err)
}
}()
if err := s.userService.UpdateLastSeenAt(context.Background(),
&user.UpdateUserLastSeenAtCommand{UserID: userID, OrgID: r.OrgID}); err != nil &&
!errors.Is(err, user.ErrLastSeenUpToDate) {
s.log.Error("Failed to update last_seen_at", "err", err, "userId", userID)
}
}(userID)
return nil
}
func (s *UserSync) EnableUserHook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error {
if !identity.ClientParams.EnableUser {
return nil
}
if !identity.ID.IsNamespace(authn.NamespaceUser) {
return nil
}
userID, err := identity.ID.ParseInt()
if err != nil {
s.log.FromContext(ctx).Warn("got invalid identity ID", "id", identity.ID, "err", err)
return nil
}
isDisabled := false
return s.userService.Update(ctx, &user.UpdateUserCommand{UserID: userID, IsDisabled: &isDisabled})
}
func (s *UserSync) upsertAuthConnection(ctx context.Context, userID int64, identity *authn.Identity, createConnection bool) error {
if identity.AuthenticatedBy == "" {
return nil
}
// If a user does not a connection to a specific auth module, create it.
// This can happen when: using multiple auth client where the same user exists in several or
// changing to new auth client
if createConnection {
return s.authInfoService.SetAuthInfo(ctx, &login.SetAuthInfoCommand{
UserId: userID,
AuthModule: identity.AuthenticatedBy,
AuthId: identity.AuthID,
OAuthToken: identity.OAuthToken,
})
}
s.log.FromContext(ctx).Debug("Updating auth connection for user", "id", identity.ID)
return s.authInfoService.UpdateAuthInfo(ctx, &login.UpdateAuthInfoCommand{
UserId: userID,
AuthId: identity.AuthID,
AuthModule: identity.AuthenticatedBy,
OAuthToken: identity.OAuthToken,
})
}
func (s *UserSync) updateUserAttributes(ctx context.Context, usr *user.User, id *authn.Identity, userAuth *login.UserAuth) error {
if errProtection := s.userProtectionService.AllowUserMapping(usr, id.AuthenticatedBy); errProtection != nil {
return errUserProtection.Errorf("user mapping not allowed: %w", errProtection)
}
// sync user info
updateCmd := &user.UpdateUserCommand{
UserID: usr.ID,
}
needsUpdate := false
if id.Login != "" && id.Login != usr.Login {
updateCmd.Login = id.Login
usr.Login = id.Login
needsUpdate = true
}
if id.Email != "" && id.Email != usr.Email {
updateCmd.Email = id.Email
usr.Email = id.Email
// If we get a new email for a user we need to mark it as non-verified.
verified := false
updateCmd.EmailVerified = &verified
usr.EmailVerified = verified
needsUpdate = true
}
if id.Name != "" && id.Name != usr.Name {
updateCmd.Name = id.Name
usr.Name = id.Name
needsUpdate = true
}
// Sync isGrafanaAdmin permission
if id.IsGrafanaAdmin != nil && *id.IsGrafanaAdmin != usr.IsAdmin {
updateCmd.IsGrafanaAdmin = id.IsGrafanaAdmin
usr.IsAdmin = *id.IsGrafanaAdmin
needsUpdate = true
}
if needsUpdate {
s.log.FromContext(ctx).Debug("Syncing user info", "id", id.ID, "update", fmt.Sprintf("%v", updateCmd))
if err := s.userService.Update(ctx, updateCmd); err != nil {
return err
}
}
return s.upsertAuthConnection(ctx, usr.ID, id, userAuth == nil)
}
func (s *UserSync) createUser(ctx context.Context, id *authn.Identity) (*user.User, error) {
// FIXME(jguer): this should be done in the user service
// quota check: we can have quotas on both global and org level
// therefore we need to query check quota for both user and org services
for _, srv := range []string{user.QuotaTargetSrv, org.QuotaTargetSrv} {
limitReached, errLimit := s.quotaService.CheckQuotaReached(ctx, quota.TargetSrv(srv), nil)
if errLimit != nil {
s.log.FromContext(ctx).Error("Failed to check quota", "error", errLimit)
return nil, errSyncUserInternal.Errorf("%w", errGettingUserQuota)
}
if limitReached {
return nil, errSyncUserForbidden.Errorf("%w", errUsersQuotaReached)
}
}
isAdmin := false
if id.IsGrafanaAdmin != nil {
isAdmin = *id.IsGrafanaAdmin
}
usr, errCreateUser := s.userService.Create(ctx, &user.CreateUserCommand{
Login: id.Login,
Email: id.Email,
Name: id.Name,
IsAdmin: isAdmin,
SkipOrgSetup: len(id.OrgRoles) > 0,
})
if errCreateUser != nil {
return nil, errCreateUser
}
err := s.upsertAuthConnection(ctx, usr.ID, id, true)
if err != nil {
return nil, err
}
return usr, nil
}
func (s *UserSync) getUser(ctx context.Context, identity *authn.Identity) (*user.User, *login.UserAuth, error) {
// Check auth info fist
if identity.AuthID != "" && identity.AuthenticatedBy != "" {
query := &login.GetAuthInfoQuery{AuthId: identity.AuthID, AuthModule: identity.AuthenticatedBy}
authInfo, errGetAuthInfo := s.authInfoService.GetAuthInfo(ctx, query)
if errGetAuthInfo != nil && !errors.Is(errGetAuthInfo, user.ErrUserNotFound) {
return nil, nil, errGetAuthInfo
}
if !errors.Is(errGetAuthInfo, user.ErrUserNotFound) {
usr, errGetByID := s.userService.GetByID(ctx, &user.GetUserByIDQuery{ID: authInfo.UserId})
if errGetByID == nil {
return usr, authInfo, nil
}
if !errors.Is(errGetByID, user.ErrUserNotFound) {
return nil, nil, errGetByID
}
// if the user connected to user auth does not exist try to clean it up
if errors.Is(errGetByID, user.ErrUserNotFound) {
if err := s.authInfoService.DeleteUserAuthInfo(ctx, authInfo.UserId); err != nil {
s.log.FromContext(ctx).Error("Failed to clean up user auth", "error", err, "auth_module", identity.AuthenticatedBy, "auth_id", identity.AuthID)
}
}
}
}
// Check user table to grab existing user
usr, err := s.lookupByOneOf(ctx, identity.ClientParams.LookUpParams)
if err != nil {
return nil, nil, err
}
var userAuth *login.UserAuth
// Special case for generic oauth: generic oauth does not store authID,
// so we need to find the user first then check for the userAuth connection by module and userID
if identity.AuthenticatedBy == login.GenericOAuthModule {
query := &login.GetAuthInfoQuery{AuthModule: identity.AuthenticatedBy, UserId: usr.ID}
userAuth, err = s.authInfoService.GetAuthInfo(ctx, query)
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
return nil, nil, err
}
}
return usr, userAuth, nil
}
func (s *UserSync) lookupByOneOf(ctx context.Context, params login.UserLookupParams) (*user.User, error) {
var usr *user.User
var err error
// If not found, try to find the user by email address
if params.Email != nil && *params.Email != "" {
usr, err = s.userService.GetByEmail(ctx, &user.GetUserByEmailQuery{Email: *params.Email})
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
return nil, err
}
}
// If not found, try to find the user by login
if usr == nil && params.Login != nil && *params.Login != "" {
usr, err = s.userService.GetByLogin(ctx, &user.GetUserByLoginQuery{LoginOrEmail: *params.Login})
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
return nil, err
}
}
if usr == nil || usr.ID == 0 { // id check as safeguard against returning empty user
return nil, user.ErrUserNotFound
}
return usr, nil
}
// syncUserToIdentity syncs a user to an identity.
// This is used to update the identity with the latest user information.
func syncUserToIdentity(usr *user.User, id *authn.Identity) {
id.ID = authn.NewNamespaceID(authn.NamespaceUser, usr.ID)
id.UID = authn.NewNamespaceIDString(authn.NamespaceUser, usr.UID)
id.Login = usr.Login
id.Email = usr.Email
id.Name = usr.Name
id.EmailVerified = usr.EmailVerified
id.IsGrafanaAdmin = &usr.IsAdmin
}
// syncSignedInUserToIdentity syncs a user to an identity.
func syncSignedInUserToIdentity(usr *user.SignedInUser, identity *authn.Identity) {
var ns authn.Namespace
if identity.ID.IsNamespace(authn.NamespaceServiceAccount) {
ns = authn.NamespaceServiceAccount
} else {
ns = authn.NamespaceUser
}
identity.UID = authn.NewNamespaceIDString(ns, usr.UserUID)
identity.Name = usr.Name
identity.Login = usr.Login
identity.Email = usr.Email
identity.OrgID = usr.OrgID
identity.OrgName = usr.OrgName
identity.OrgRoles = map[int64]org.RoleType{identity.OrgID: usr.OrgRole}
identity.HelpFlags1 = usr.HelpFlags1
identity.Teams = usr.Teams
identity.LastSeenAt = usr.LastSeenAt
identity.IsDisabled = usr.IsDisabled
identity.IsGrafanaAdmin = &usr.IsGrafanaAdmin
identity.EmailVerified = usr.EmailVerified
}