-
Notifications
You must be signed in to change notification settings - Fork 1
/
user.go
449 lines (387 loc) · 16.4 KB
/
user.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
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
package pgmodels
import (
"regexp"
"strings"
"time"
"github.com/APTrust/registry/common"
"github.com/APTrust/registry/constants"
v "github.com/asaskevich/govalidator"
)
// Phone: +1234567890 (10 digits)
var rePhone = regexp.MustCompile("^\\+[0-9]{11}$")
var reNumeric = regexp.MustCompile("[^0-9]+")
const (
ErrUserName = "Name must contain at least 2 characters."
ErrUserEmail = "Email address is required."
ErrUserPhone = "Please enter a phone number in format +2125551212."
ErrUser2Factor = "Please choose yes or no."
ErrUserGracePeriod = "Enter a date specifying when this user must enable two-factor authentication."
ErrUserInst = "Please choose an institution."
ErrUserRole = "Please choose a role for this user."
ErrUserInstNotFound = "Internal error: cannot find institution."
ErrUserInvalidAdmin = "Sys Admin role is not valid for this institution."
ErrUserPwdMissing = "Encrypted password is missing."
ErrUserPwdIncorrect = "Incorrect Password."
)
// User is a person who can log in and do stuff.
// Most of this model was inherited from the old Rails app. It includes
// a number of obsolete fields that we should remove after we're stable
// in production.
type User struct {
TimestampModel
// Name is the user's display name.
Name string `json:"name" pg:"name"`
// Email is used to login. Must be unique.
Email string `json:"email" pg:"email"`
// PhoneNumber should start with + country code (e.g. +1 for US).
// This number is used for SMS two-factor auth, so it should be
// a mobile phone.
PhoneNumber string `json:"phone_number" pg:"phone_number"`
// EncryptedPassword is the user's password, encrypted.
EncryptedPassword string `json:"-" form:"-" pg:"encrypted_password"`
// ResetPasswordToken is an encrypted version of the token sent to a
// user who wants to reset their password. The plaintext version of
// this is emailed to the user.
ResetPasswordToken string `json:"-" form:"-" pg:"reset_password_token"`
// ResetPasswordSentAt is a timestamp describing when we sent a password
// reset email. The ResetPasswordToken should be valid for only a few
// minutes after this timestamp.
ResetPasswordSentAt time.Time `json:"reset_password_sent_at" form:"-" pg:"reset_password_sent_at"`
// RememberCreatedAt - ??? - Legacy field from Devise, probably related
// to time-based OTPs. Not used.
RememberCreatedAt time.Time `json:"-" form:"-" pg:"remember_created_at"`
// SignInCount is the number of times this user has successfully signed in.
SignInCount int `json:"sign_in_count" form:"-" pg:"sign_in_count,use_zero"`
// CurrentSignInAt is a timestamp describing when this user signed
// in for their current session.
CurrentSignInAt time.Time `json:"current_sign_in_at" form:"-" pg:"current_sign_in_at"`
// LastSignInAt is a timestamp describing when this user signed
// in for their previous session.
LastSignInAt time.Time `json:"last_sign_in_at" form:"-" pg:"last_sign_in_at"`
// CurrentSignInIP is the IP address from which user signed in.
CurrentSignInIP string `json:"current_sign_in_ip" form:"-" pg:"current_sign_in_ip"`
// LastSignInIP is the IP address from which user signed in for their
// prior session.
LastSignInIP string `json:"last_sign_in_ip" form:"-" pg:"last_sign_in_ip"`
// InstitituionID is the id of the institution to which this user
// belongs.
InstitutionID int64 `json:"institution_id" pg:"institution_id"`
// EncryptedAPISecretKey is the user's encrypted API key. This may
// be empty if the user has never requested a key.
EncryptedAPISecretKey string `json:"-" form:"-" pg:"encrypted_api_secret_key"`
// PasswordChangedAt is the date and time the user last changed their
// password.
PasswordChangedAt time.Time `json:"password_changed_at" form:"-" pg:"password_changed_at"`
// EncryptedOTPSecret is the encrypted version of a user's one-time
// password. The plaintext is usually a six-digit code sent via SMS.
// This will be empty if we didn't send a code, or if the user has
// correctly entered it and completed two-factor login.
EncryptedOTPSecret string `json:"-" form:"-" pg:"encrypted_otp_secret"`
// EncryptedOTPSecretIV is a legacy field from Devise. Not used.
// TODO: Delete this.
EncryptedOTPSecretIV string `json:"-" form:"-" pg:"encrypted_otp_secret_iv"`
// EncryptedOTPSecretSalt is a legacy field from Devise. Not used.
EncryptedOTPSecretSalt string `json:"-" form:"-" pg:"encrypted_otp_secret_salt"`
// EncryptedOTPSentAt describes when we sent a one-time password
// via text/SMS. We track this because these passwords should be
// valid only for a limited time. This field be empty except when
// we're waiting for a user to enter a text/SMS OTP.
EncryptedOTPSentAt time.Time `json:"-" form:"-" pg:"encrypted_otp_sent_at"`
// ConsumedTimestep is a legacy field from Devise, which used it
// for time-based one-time passwords. Not used.
// TODO: Delete this.
ConsumedTimestep int `json:"-" form:"-" pg:"consumed_timestep"`
// OTPRequiredForLogin indicates whether, as a matter of policy, the
// user must use some form of OTP to log in. If true, the user should
// be allowed to log in only with Authy one-touch, six-digit SMS
// code, or backup code.
//
// Use IsTwoFactorUser() to actually determine whether we should
// force this user to use Authy, SMS, or backup code to get it.
// This is messy because it has to mirror the legacy Rails implementation
// for now.
OTPRequiredForLogin bool `json:"otp_required_for_login" pg:"otp_required_for_login"`
// DeactivatedAt is a timestamp describing when this user was
// deactivated. For most users, it will be empty. APTrust admin and
// Institutional Admin can disable other users. Then can also
// re-enable a user by clearing this flag.
DeactivatedAt time.Time `json:"deactivated_at" form:"-" pg:"deactivated_at"`
// EnabledTwoFactor indicates whether the user's account has
// enabled two factor authentication. See also ConfirmedTwoFactor.
EnabledTwoFactor bool `json:"enabled_two_factor" form:"-" pg:"enabled_two_factor"`
// ConfirmedTwoFactor indicates that the system has confirmed this
// user's phone number (for SMS) and Authy account (for Authy 2FA).
ConfirmedTwoFactor bool `json:"confirmed_two_factor" form:"-" pg:"confirmed_two_factor"`
// OTPBackupCodes is a list of backup codes the user can use to
// log if they can't get in via SMS or Authy.
OTPBackupCodes []string `json:"-" form:"-" pg:"otp_backup_codes,array"`
// AuthyID is the user's Authy ID. We need this to send them push
// messages to complete one-touch sign-in. This will be empty for
// those who don't use Authy.
AuthyID string `json:"-" form:"-" pg:"authy_id"`
// LastSignInWithAuthy is the timestamp of this user's last
// successful sign-in with Authy.
LastSignInWithAuthy time.Time `json:"last_sign_in_with_authy" form:"-" pg:"last_sign_in_with_authy"`
// AuthyStatus indicates how the user wants to authenticate with
// Authy. If it's constants.TwoFactorAuthy, we should send them a
// push, so they can login with one-touch. Anything else means SMS,
// but call IsTwoFactorUser() to make sure they're actually require
// two-factor auth before trying to text them.
AuthyStatus string `json:"authy_status" pg:"authy_status"`
// EmailVerified will be true once the system has verified that the
// user's email address is correct.
EmailVerified bool `json:"email_verified" form:"-" pg:"email_verified"`
// InitialPasswordUpdated will be true once a user updates their
// initial password. When we create a new user, we generate a random
// password, then send them an email with a login link. At that
// point, the user has to set their own password.
InitialPasswordUpdated bool `json:"initial_password_updated" form:"-" pg:"initial_password_updated"`
// ForcePasswordUpdate indicated whether the user will be forced to
// update their password the next time they visit.
ForcePasswordUpdate bool `json:"force_password_update" form:"-" pg:"force_password_update"`
// GracePeriod is a legacy field from the old Rails app. It held the
// date by which a user MUST complete either Authy or SMS two-factor
// setup. This feature was universally despised, and we won't be
// using it unless someone complains. For now, consider this as
// unused.
GracePeriod time.Time `json:"grace_period" time_format:"2006-01-02" pg:"grace_period"`
// AwaitingSecondFactor indicates that the user has logged in with
// email and password, but has not yet completed the second login
// step (Authy, SMS OTP, or backup code). This flag is only set on
// users going through the two-factor process. Middleware checks it
// to prevent partially logged-in users from accessing any pages other
// than those required to complete the two-factor login process.
AwaitingSecondFactor bool `json:"-" pg:"awaiting_second_factor,use_zero"`
// Role is the user's role.
Role string `json:"role" pg:"role"`
// Institution is where they lock you up after you've spent too much
// time trying to figure out the old Rails code.
Institution *Institution `json:"institution" pg:"rel:has-one"`
}
// UserByID returns the institution with the specified id.
// Returns pg.ErrNoRows if there is no match.
func UserByID(id int64) (*User, error) {
query := NewQuery().Where(`"user"."id"`, "=", id)
return UserGet(query)
}
// UserByEmail returns the user with the specified email address.
// Returns pg.ErrNoRows if there is no match.
func UserByEmail(email string) (*User, error) {
query := NewQuery().Where(`"user"."email"`, "=", email)
return UserGet(query)
}
// UserGet returns the first user matching the query.
func UserGet(query *Query) (*User, error) {
var user User
err := query.Relations("Institution").Select(&user)
return &user, err
}
// UserSelect returns all user matching the query.
func UserSelect(query *Query) ([]*User, error) {
var users []*User
err := query.Select(&users)
return users, err
}
// UserSignIn signs a user in. If successful, it returns the User
// record with User.Institution properly set. If it fails, check
// the error.
func UserSignIn(email, password, ipAddr string) (*User, error) {
user, err := UserByEmail(email)
if IsNoRowError(err) {
return nil, common.ErrInvalidLogin
} else if err != nil {
return nil, err
}
if !user.DeactivatedAt.IsZero() {
return nil, common.ErrAccountDeactivated
}
if !common.ComparePasswords(user.EncryptedPassword, password) {
common.Context().Log.Warn().Msgf("Wrong password for user %s", email)
return nil, common.ErrInvalidLogin
}
user.SignInCount = user.SignInCount + 1
if user.CurrentSignInIP != "" {
user.LastSignInIP = user.CurrentSignInIP
}
if user.CurrentSignInAt.IsZero() {
user.LastSignInAt = user.CurrentSignInAt
}
user.CurrentSignInIP = ipAddr
user.CurrentSignInAt = time.Now().UTC()
err = user.Save()
return user, err
}
// UserSignOut signs a user out.
func (user *User) SignOut() error {
if user.CurrentSignInIP != "" {
user.LastSignInIP = user.CurrentSignInIP
}
if !user.CurrentSignInAt.IsZero() {
user.LastSignInAt = user.CurrentSignInAt
}
user.CurrentSignInIP = ""
user.CurrentSignInAt = time.Time{}
return user.ClearOTPSecret()
}
func (user *User) Save() error {
user.SetTimestamps()
user.ReformatPhone()
err := user.Validate()
if err != nil {
return err
}
if user.ID == int64(0) {
return insert(user)
}
return update(user)
}
func (user *User) Delete() error {
user.DeactivatedAt = time.Now().UTC()
return update(user)
}
func (user *User) Undelete() error {
user.DeactivatedAt = time.Time{}
return update(user)
}
func (user *User) Validate() *common.ValidationError {
errors := make(map[string]string)
if !v.IsByteLength(user.Name, 2, 200) {
errors["Name"] = ErrUserName
}
if !v.IsEmail(user.Email) {
errors["Email"] = ErrUserEmail
}
if user.PhoneNumber != "" && !rePhone.MatchString(user.PhoneNumber) {
errors["PhoneNumber"] = ErrUserPhone
}
if user.OTPRequiredForLogin && user.GracePeriod.IsZero() {
errors["GracePeriod"] = ErrUserGracePeriod
}
if user.InstitutionID < int64(1) {
errors["InstitutionID"] = ErrUserInst
}
if !v.IsIn(user.Role, constants.Roles...) {
errors["Role"] = ErrUserRole
}
if user.Role == constants.RoleSysAdmin {
aptrust, err := InstitutionByIdentifier("aptrust.org")
if err != nil {
errors["Role"] = ErrUserInstNotFound
} else if user.InstitutionID != aptrust.ID {
errors["Role"] = ErrUserInvalidAdmin
}
}
if user.EncryptedPassword == "" {
errors["EncryptedPassword"] = ErrUserPwdMissing
}
if len(errors) > 0 {
return &common.ValidationError{Errors: errors}
}
return nil
}
func (user *User) ReformatPhone() {
digitsOnly := reNumeric.ReplaceAllString(user.PhoneNumber, "")
if len(digitsOnly) > 0 {
if !strings.HasPrefix(digitsOnly, "1") {
digitsOnly = "1" + digitsOnly
}
user.PhoneNumber = "+" + digitsOnly
} else {
user.PhoneNumber = digitsOnly // may be blank string
}
}
// HasPermission returns true or false to indicate whether the user has
// sufficient permissions to perform the requested action. Param action
// should be one of the constants from constants/permissions.go. Param
// institutionID should be the ID of the institution that owns the object
// upon which the user is trying to act. In certain cases, such as when a
// user is editing him/herself, this can be zero.
func (user *User) HasPermission(action constants.Permission, institutionID int64) bool {
// Sys admin's permissions apply across all institutional boundaries.
if user.IsAdmin() {
return constants.CheckPermission(user.Role, action)
}
// Institutional user and admin permissions apply only within their
// own institutions.
return user.InstitutionID == institutionID && constants.CheckPermission(user.Role, action)
}
// IsAdmin returns true if user is a Sys Admin. Returns false for all other
// roles (including institutional admin, which is not a super user).
func (user *User) IsAdmin() bool {
return user.Role == constants.RoleSysAdmin
}
// IsSMSUser returns true if this user has enabled two-factor authentication
// with SMS/text message.
func (user *User) IsSMSUser() bool {
return user.IsTwoFactorUser() && (user.AuthyStatus == constants.TwoFactorSMS || user.AuthyStatus == "")
}
// IsAuthyOneTouchUser returns true if the user has enabled Authy one touch
// for two-factor login.
func (user *User) IsAuthyOneTouchUser() bool {
return user.IsTwoFactorUser() && (user.AuthyStatus == constants.TwoFactorAuthy)
}
// IsTwoFactorUser returns true if this user has enabled and confirmed
// two factor authentication.
//
// Sorry... For now, we have to work with the convoluted logic of the
// old Rails app. Hence the confusion between this and OTPRequiredForLogin.
func (user *User) IsTwoFactorUser() bool {
return user.EnabledTwoFactor && user.ConfirmedTwoFactor
}
// TwoFactorMethod returns one of the following:
//
// constants.TwoFactorNone if the user does not use two-factor auth.
//
// constants.TwoFactorAuthy if the user uses two-factor auth via Authy.
//
// constants.TwoFactorSMS if the user receives two-factor OTP code via
// text/SMS
func (user *User) TwoFactorMethod() string {
if !user.IsTwoFactorUser() {
return constants.TwoFactorNone
}
if user.IsSMSUser() {
return constants.TwoFactorSMS
}
return constants.TwoFactorAuthy
}
// CreateOTPToken creates a new one-time password token, typically
// used for SMS-based two-factor authentication. It saves an
// encrypted version of the token to the database and returns
// the plaintext version of the token. For SMS, we use six-digit
// tokens because they're easy for a user to type.
func (user *User) CreateOTPToken() (string, error) {
token, err := common.NewOTP()
if err != nil {
return "", err
}
encryptedToken, err := common.EncryptPassword(token)
if err != nil {
return "", err
}
user.EncryptedOTPSecret = encryptedToken
err = user.Save()
if err != nil {
return "", err
}
return token, err
}
// ClearOTPSecret deletes the user's EncryptedOTPSecret.
func (user *User) ClearOTPSecret() error {
user.EncryptedOTPSecret = ""
user.EncryptedOTPSentAt = time.Time{}
return user.Save()
}
// CountryCodeAndPhone returns this user's country code and phone number.
func (user *User) CountryCodeAndPhone() (int32, string, error) {
return common.CountryCodeAndPhone(user.PhoneNumber)
}
// HasUnreadAlerts returns true if user has unread alerts.
func (user *User) HasUnreadAlerts() bool {
alertView := &AlertView{}
count, err := NewQuery().Where("user_id", "=", user.ID).IsNull("read_at").Count(alertView)
if err != nil {
common.Context().Log.Warn().Msgf("Error checking unread alerts for user %s: %v", user.Email, err)
}
return count > 0
}