Files
im_server/server/user.go
T

1474 lines
46 KiB
Go

package main
import (
"container/heap"
crand "crypto/rand"
"math/rand"
"strings"
"time"
"slices"
"github.com/tinode/chat/server/auth"
"github.com/tinode/chat/server/logs"
"github.com/tinode/chat/server/push"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types"
)
const (
// Unread counter update return codes.
// Counter not initialized, IO pending.
unreadUpdateIOPending = -1
// Counter initialization error.
unreadUpdateError = -2
)
// Process request for a new account.
func replyCreateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
// The session cannot authenticate with the new account because it's already authenticated.
if msg.Acc.Login && (!s.uid.IsZero() || rec != nil) {
s.queueOut(ErrAlreadyAuthenticated(msg.Id, "", msg.Timestamp))
logs.Warn.Println("create user: login requested while authenticated, sid=", s.sid)
return
}
// Find authenticator for the requested scheme.
authhdl := store.Store.GetLogicalAuthHandler(msg.Acc.Scheme)
if authhdl == nil || msg.Acc.Scheme != "basic" {
// New accounts must have an authentication scheme
s.queueOut(ErrMalformed(msg.Id, "", msg.Timestamp))
logs.Warn.Println("create user: unknown auth handler, sid=", s.sid)
return
}
account, phone, secretAccount, phoneRegistration, err := resolveRegistrationAccount(msg.Acc.Username, msg.Acc.Phone)
if err != nil || !registrationSecretMatches(msg.Acc.Secret, secretAccount) {
logs.Warn.Println("create user: invalid registration account fields, sid=", s.sid)
s.queueOut(decodeStoreError(types.ErrPolicy, msg.Id, msg.Timestamp,
map[string]any{"what": "account", "reason": "REGISTRATION_ACCOUNT_INVALID"}))
return
}
authSecret := msg.Acc.Secret
if account != secretAccount {
authSecret = registrationSecretForAccount(msg.Acc.Secret, account)
}
inviteCode := strings.TrimSpace(msg.Acc.InviteCode)
if msg.Acc.InviteCode != inviteCode || (inviteCode != "" && !isValidIMInviteCode(inviteCode)) {
s.queueOut(decodeStoreError(types.ErrPolicy, msg.Id, msg.Timestamp,
map[string]any{"what": "invite_code", "reason": "INVITE_CODE_INVALID"}))
return
}
// Check if login is unique and compliance with the policy (not too long or too short).
authCtx := auth.AuthContext{TenantID: s.tenantID, RemoteAddr: s.remoteAddr}
for attempts := 0; ; attempts++ {
if ok, err := authhdl.IsUnique(authCtx, authSecret); ok {
break
} else if account != secretAccount && err == types.ErrDuplicate && attempts < 8 {
account, err = randomRegistrationUsername()
if err != nil {
logs.Warn.Println("create user: failed to generate username", err, "sid=", s.sid)
s.queueOut(ErrUnknown(msg.Id, "", msg.Timestamp))
return
}
authSecret = registrationSecretForAccount(msg.Acc.Secret, account)
continue
} else {
logs.Warn.Println("create user: auth secret is not compliant", err, "sid=", s.sid)
s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp,
map[string]any{"what": "auth"}))
return
}
}
var user types.User
user.TenantID = s.tenantID
user.Username = account
user.Phone = phone
var private any
// If account state is being assigned, make sure the sender is a root user.
if msg.Acc.State != "" {
if auth.Level(msg.AuthLvl) != auth.LevelRoot {
logs.Warn.Println("create user: attempt to set account state by non-root, sid=", s.sid)
msg := ErrPermissionDenied(msg.Id, "", msg.Timestamp)
msg.Ctrl.Params = map[string]any{"what": "state"}
s.queueOut(msg)
return
}
state, err := types.NewObjState(msg.Acc.State)
if err != nil || state == types.StateUndefined || state == types.StateDeleted {
logs.Warn.Println("create user: invalid account state", err, "sid=", s.sid)
s.queueOut(ErrMalformed(msg.Id, "", msg.Timestamp))
return
}
user.State = state
}
if msg.Acc.Tags != nil {
logs.Warn.Println("create user: user tags are no longer supported, sid=", s.sid)
msg := ErrPermissionDenied(msg.Id, "", msg.Timestamp)
msg.Ctrl.Params = map[string]any{"what": "tags"}
s.queueOut(msg)
return
}
// Pre-check credentials for validity. We don't know user's access level
// consequently cannot check presence of required credentials. Must do that later.
creds := normalizeCredentials(msg.Acc.Cred, true)
var registrationPhone string
var registrationTelCredential bool
for i := range creds {
cr := &creds[i]
vld := store.Store.GetValidator(cr.Method)
if vld == nil || !vld.IsInitialized() {
s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp,
map[string]any{"what": cr.Method}))
return
}
normalized, err := vld.PreCheck(cr.Value, cr.Params)
if err != nil {
logs.Warn.Println("create user: failed credential pre-check", cr, err, "sid=", s.sid)
s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp,
map[string]any{"what": cr.Method}))
return
}
if cr.Method == "tel" {
phone := strings.TrimSpace(strings.TrimPrefix(normalized, "tel:"))
challengeID, _ := cr.Params["challenge_id"].(string)
if phone == "" || strings.TrimSpace(challengeID) == "" || strings.TrimSpace(cr.Response) == "" ||
(registrationPhone != "" && registrationPhone != phone) {
s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp,
map[string]any{"creds": []string{"tel"}}))
return
}
registrationPhone = phone
registrationTelCredential = true
}
}
if !phoneRegistration && registrationTelCredential {
logs.Warn.Println("create user: username registration cannot attach a phone credential, sid=", s.sid)
s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp,
map[string]any{"creds": []string{"tel"}, "reason": "PHONE_NOT_SELECTED"}))
return
}
if phoneRegistration && registrationTelCredential && registrationPhone != phone {
s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp,
map[string]any{"creds": []string{"tel"}, "reason": "PHONE_MISMATCH"}))
return
}
smsEnabled := registrationSMSEnabled(time.Now().UTC())
if phoneRegistration && smsEnabled && !registrationTelCredential {
logs.Warn.Println("create user: verified phone is required while registration SMS is enabled, sid=", s.sid)
s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp,
map[string]any{"creds": []string{"tel"}, "reason": "PHONE_VERIFICATION_REQUIRED"}))
return
}
inviteRequired := !phoneRegistration || (!smsEnabled && !registrationTelCredential)
if inviteRequired && inviteCode == "" {
s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp,
map[string]any{"what": "invite_code", "reason": "INVITE_CODE_REQUIRED"}))
return
}
if inviteCode != "" {
if err := validateRegistrationInvite(s.tenantID, inviteCode, time.Now().UTC()); err != nil {
s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp,
map[string]any{"what": "invite_code", "reason": registrationInviteReason(err)}))
return
}
}
// Assign default access values in case the acc creator has not provided them
user.Access.Auth = getDefaultAccess(types.TopicCatP2P, true, false) |
getDefaultAccess(types.TopicCatGrp, true, false)
user.Access.Anon = getDefaultAccess(types.TopicCatP2P, false, false) |
getDefaultAccess(types.TopicCatGrp, false, false)
// Assign actual access values, public and private.
if msg.Acc.Desc != nil {
if msg.Acc.Desc.DefaultAcs != nil {
if msg.Acc.Desc.DefaultAcs.Auth != "" {
user.Access.Auth.UnmarshalText([]byte(msg.Acc.Desc.DefaultAcs.Auth))
user.Access.Auth &= globals.typesModeCP2P
if user.Access.Auth != types.ModeNone {
user.Access.Auth |= types.ModeApprove
}
}
if msg.Acc.Desc.DefaultAcs.Anon != "" {
user.Access.Anon.UnmarshalText([]byte(msg.Acc.Desc.DefaultAcs.Anon))
user.Access.Anon &= globals.typesModeCP2P
if user.Access.Anon != types.ModeNone {
user.Access.Anon |= types.ModeApprove
}
}
}
if !isNullValue(msg.Acc.Desc.Public) {
user.Public = msg.Acc.Desc.Public
}
if !isNullValue(msg.Acc.Desc.Private) {
private = msg.Acc.Desc.Private
}
}
user.Public = registrationPublicWithUsername(user.Public, user.Username)
// Create user record in the database.
if _, err := store.Users.Create(s.tenantID, &user, private); err != nil {
logs.Warn.Println("create user: failed to create user", err, "sid=", s.sid)
s.queueOut(ErrUnknown(msg.Id, "", msg.Timestamp))
return
}
// Add authentication record.
rec, err = authhdl.AddRecord(authCtx,
&auth.Rec{TenantID: s.tenantID, Uid: user.Uid()}, authSecret)
if err != nil {
logs.Warn.Println("create user: add auth record failed", err, "sid=", s.sid)
s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp, nil))
// Attempt to delete incomplete user record
if err = store.Users.Delete(s.tenantID, user.Uid(), true); err != nil {
logs.Warn.Println("create user: failed to delete incomplete user record", err, "sid=", s.sid)
}
return
}
// When creating an account, the user must provide all required credentials.
// If any are missing, reject the request.
if len(creds) < len(globals.authValidators[rec.AuthLevel]) {
logs.Warn.Println("create user: missing credentials; have:", creds, "want:",
globals.authValidators[rec.AuthLevel], s.sid)
_, missing, _ := stringSliceDelta(globals.authValidators[rec.AuthLevel], credentialMethods(creds))
s.queueOut(decodeStoreError(types.ErrPolicy, msg.Id, msg.Timestamp,
map[string]any{"creds": missing}))
// Attempt to delete incomplete user record
if err = store.Users.Delete(s.tenantID, user.Uid(), true); err != nil {
logs.Warn.Println("create user: failed to delete incomplete user record", err, "sid=", s.sid)
}
return
}
// Save and validate credentials.
tmpToken, _, _ := store.Store.GetLogicalAuthHandler("token").GenSecret(&auth.Rec{
TenantID: s.tenantID,
Uid: user.Uid(),
AuthLevel: auth.LevelAuth,
Lifetime: auth.Duration(time.Hour * 24),
})
validated, _, err := addCreds(s.tenantID, user.Uid(), creds, nil, s.lang, tmpToken)
if err != nil {
logs.Warn.Println("create user: failed to save or validate credential", err, "sid=", s.sid)
s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp, nil))
// Delete incomplete user record.
if err = store.Users.Delete(s.tenantID, user.Uid(), true); err != nil {
logs.Warn.Println("create user: failed to delete incomplete user record", err, "sid=", s.sid)
}
return
}
if registrationTelCredential && !slices.Contains(validated, "tel") {
logs.Warn.Println("create user: phone credential was not validated, sid=", s.sid)
s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp,
map[string]any{"creds": []string{"tel"}, "reason": "PHONE_VERIFICATION_REQUIRED"}))
if err = store.Users.Delete(s.tenantID, user.Uid(), true); err != nil {
logs.Warn.Println("create user: failed to delete incomplete user record", err, "sid=", s.sid)
}
return
}
if inviteCode != "" {
requestID := strings.TrimSpace(msg.Id)
if requestID == "" {
requestID = "acc:" + s.sid
}
if len(requestID) > 64 {
requestID = requestID[:64]
}
if err = store.RegistrationInvites.RecordUse(s.tenantID, inviteCode, user.Uid(), requestID, time.Now().UTC()); err != nil {
logs.Warn.Println("create user: record invite use failed", err, "sid=", s.sid)
s.queueOut(decodeStoreError(types.ErrCredentials, msg.Id, msg.Timestamp,
map[string]any{"what": "invite_code", "reason": registrationInviteReason(err)}))
if deleteErr := store.Users.Delete(s.tenantID, user.Uid(), true); deleteErr != nil {
logs.Warn.Println("create user: failed to delete incomplete user record", deleteErr, "sid=", s.sid)
}
return
}
dispatchIMBusinessEvents(s.tenantID)
}
if msg.Extra != nil && len(msg.Extra.Attachments) > 0 {
if err := store.Files.LinkAttachments(s.tenantID, user.Uid().UserId(), types.ZeroUid, msg.Extra.Attachments); err != nil {
logs.Warn.Println("create user: failed to link avatar attachment", err, "sid=", s.sid)
// This is not a critical error, continue execution.
}
}
var reply *ServerComMessage
if msg.Acc.Login {
// Process user's login request.
_, missing, _ := stringSliceDelta(globals.authValidators[rec.AuthLevel], validated)
reply = s.onLogin(msg.Id, msg.Timestamp, rec, missing, msg.Acc.Scheme, account)
} else {
// Not using the new account for logging in.
reply = NoErrCreated(msg.Id, "", msg.Timestamp)
reply.Ctrl.Params = map[string]any{
"user": user.Uid().UserId(),
"authlvl": rec.AuthLevel.String(),
}
}
params := reply.Ctrl.Params.(map[string]any)
params["username"] = user.Username
params["desc"] = &MsgTopicDesc{
CreatedAt: &user.CreatedAt,
UpdatedAt: &user.UpdatedAt,
DefaultAcs: &MsgDefaultAcsMode{
Auth: user.Access.Auth.String(),
Anon: user.Access.Anon.String(),
},
Public: user.Public,
Private: private,
}
s.queueOut(reply)
pluginAccount(&user, plgActCreate)
}
func resolveRegistrationAccount(username, phone string) (account, canonicalPhone, secretAccount string, phoneRegistration bool, err error) {
if phone != "" {
if len(phone) != 11 || !asciiRegistrationDigits(phone) {
return "", "", "", false, types.ErrPolicy
}
canonicalPhone, err = normalizeIMPhone(phone, "CN")
if err != nil {
return "", "", "", false, err
}
if username == "" {
account, err = randomRegistrationUsername()
if err != nil {
return "", "", "", false, err
}
return account, canonicalPhone, canonicalPhone, true, nil
}
if err = validateRegistrationUsername(username); err != nil {
return "", "", "", false, err
}
return username, canonicalPhone, username, true, nil
}
if err = validateRegistrationUsername(username); err != nil {
return "", "", "", false, err
}
return username, "", username, false, nil
}
func validateRegistrationUsername(username string) error {
if username == "" || len(username) > 10 {
return types.ErrPolicy
}
for i := range username {
if (username[i] < 'a' || username[i] > 'z') && (username[i] < '0' || username[i] > '9') {
return types.ErrPolicy
}
}
return nil
}
func asciiRegistrationDigits(value string) bool {
for i := range value {
if value[i] < '0' || value[i] > '9' {
return false
}
}
return value != ""
}
func registrationSecretMatches(secret []byte, account string) bool {
separator := strings.IndexByte(string(secret), ':')
return separator > 0 && string(secret[:separator]) == account
}
func registrationSecretForAccount(secret []byte, account string) []byte {
separator := strings.IndexByte(string(secret), ':')
if separator < 0 {
return secret
}
return []byte(account + string(secret[separator:]))
}
func randomRegistrationUsername() (string, error) {
const letters = "abcdefghijklmnopqrstuvwxyz"
const digits = "0123456789"
var raw [9]byte
if _, err := crand.Read(raw[:]); err != nil {
return "", err
}
out := make([]byte, 9)
for i := 0; i < 3; i++ {
out[i] = letters[int(raw[i])%len(letters)]
}
for i := 3; i < 9; i++ {
out[i] = digits[int(raw[i])%len(digits)]
}
return string(out), nil
}
func registrationPublicWithUsername(public any, username string) any {
if username == "" {
return public
}
if public == nil {
return map[string]any{"fn": username}
}
if values, ok := public.(map[string]any); ok {
values["fn"] = username
return values
}
return map[string]any{"fn": username}
}
func validateRegistrationInvite(tenantID types.TenantID, code string, now time.Time) error {
if store.RegistrationInvites == nil {
return types.ErrInternal
}
invite, err := store.RegistrationInvites.GetByCode(tenantID, code)
if err != nil {
return err
}
if invite == nil {
return types.ErrInviteInvalid
}
switch invite.Status {
case types.RegistrationInviteRevoked:
return types.ErrInviteRevoked
case types.RegistrationInviteExpired:
return types.ErrInviteExpired
case types.RegistrationInviteActive:
if !now.Before(invite.ExpiresAt) {
return types.ErrInviteExpired
}
return nil
default:
return types.ErrInviteInvalid
}
}
func registrationInviteReason(err error) string {
switch err {
case types.ErrInviteExpired:
return "INVITE_CODE_EXPIRED"
case types.ErrInviteRevoked:
return "INVITE_CODE_REVOKED"
default:
return "INVITE_CODE_INVALID"
}
}
// Process update to an account:
// * Authentication update, i.e. login/password change
// * Credentials update
func replyUpdateUser(s *Session, msg *ClientComMessage, rec *auth.Rec) {
if s.uid.IsZero() && rec == nil {
// Session is not authenticated and no temporary auth is provided.
logs.Warn.Println("replyUpdateUser: not a new account and not authenticated", s.sid)
s.queueOut(ErrPermissionDenied(msg.Id, "", msg.Timestamp))
return
} else if msg.AsUser != "" && rec != nil {
// Two UIDs: one from msg.from, one from temporary auth. Ambigous, reject.
logs.Warn.Println("replyUpdateUser: got both authenticated session and token", s.sid)
s.queueOut(ErrMalformed(msg.Id, "", msg.Timestamp))
return
}
userId := msg.AsUser
authLvl := auth.Level(msg.AuthLvl)
if rec != nil {
userId = rec.Uid.UserId()
authLvl = rec.AuthLevel
}
if msg.Acc.User != "" && msg.Acc.User != userId {
if s.authLvl != auth.LevelRoot {
logs.Warn.Println("replyUpdateUser: attempt to change another's account by non-root", s.sid)
s.queueOut(ErrPermissionDenied(msg.Id, "", msg.Timestamp))
return
}
// Root is editing someone else's account.
userId = msg.Acc.User
authLvl = auth.ParseAuthLevel(msg.Acc.AuthLevel)
}
uid := types.ParseUserId(userId)
if uid.IsZero() {
// msg.Acc.User contains invalid data.
s.queueOut(ErrMalformed(msg.Id, "", msg.Timestamp))
logs.Warn.Println("replyUpdateUser: user id is invalid or missing", s.sid)
return
}
// Only root can suspend accounts, including own account.
if msg.Acc.State != "" && s.authLvl != auth.LevelRoot {
s.queueOut(ErrPermissionDenied(msg.Id, "", msg.Timestamp))
logs.Warn.Println("replyUpdateUser: attempt to change account state by non-root", s.sid)
return
}
user, err := store.Users.Get(s.tenantID, uid)
if user == nil && err == nil {
err = types.ErrNotFound
}
if err != nil {
logs.Warn.Println("replyUpdateUser: failed to fetch user from DB", err, s.sid)
s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp, nil))
return
}
if user.TenantID != s.tenantID {
logs.Warn.Println("replyUpdateUser: target user is outside session tenant", s.sid)
s.queueOut(decodeStoreError(types.ErrNotFound, msg.Id, msg.Timestamp, nil))
return
}
var params map[string]any
if msg.Acc.Scheme != "" {
err = updateUserAuth(s.tenantID, msg, user, rec, s.remoteAddr)
} else if len(msg.Acc.Cred) > 0 {
if authLvl == auth.LevelNone {
// msg.Acc.AuthLevel contains invalid data.
s.queueOut(ErrMalformed(msg.Id, "", msg.Timestamp))
logs.Warn.Println("replyUpdateUser: auth level is missing", s.sid)
return
}
// Handle request to update credentials.
tmpToken, _, _ := store.Store.GetLogicalAuthHandler("token").GenSecret(&auth.Rec{
TenantID: s.tenantID,
Uid: uid,
AuthLevel: auth.LevelNone,
Lifetime: auth.Duration(time.Hour * 24),
Features: auth.FeatureNoLogin,
})
_, _, err := addCreds(s.tenantID, uid, msg.Acc.Cred, nil, s.lang, tmpToken)
if err == nil {
if allCreds, err := store.Users.GetAllCreds(s.tenantID, uid, "", true); err != nil {
var validated []string
for i := range allCreds {
validated = append(validated, allCreds[i].Method)
}
_, missing, _ := stringSliceDelta(globals.authValidators[authLvl], validated)
if len(missing) > 0 {
params = map[string]any{"cred": missing}
}
}
}
} else if msg.Acc.State != "" {
var changed bool
changed, err = changeUserState(s, uid, user, msg)
if !changed && err == nil {
s.queueOut(InfoNotModified(msg.Id, "", msg.Timestamp))
return
}
} else {
err = types.ErrMalformed
}
if err != nil {
logs.Warn.Println("replyUpdateUser: failed to update user", err, s.sid)
s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp, nil))
return
}
s.queueOut(NoErrParams(msg.Id, "", msg.Timestamp, params))
// Call plugin with the account update
pluginAccount(user, plgActUpd)
}
// Authentication update
func updateUserAuth(tenantID types.TenantID, msg *ClientComMessage, user *types.User, _ *auth.Rec,
remoteAddr string) error {
authhdl := store.Store.GetLogicalAuthHandler(msg.Acc.Scheme)
if authhdl != nil {
// Request to update auth of an existing account. Only basic & rest auth are currently supported
// TODO(gene): support adding new auth schemes
_, err := authhdl.UpdateRecord(auth.AuthContext{TenantID: tenantID, RemoteAddr: remoteAddr},
&auth.Rec{TenantID: tenantID, Uid: user.Uid()}, msg.Acc.Secret)
if err != nil {
return err
}
return nil
}
// Invalid or unknown auth scheme
return types.ErrMalformed
}
// addCreds adds credentials and re-sends validation requests for existing ones.
func addCreds(tenantID types.TenantID, uid types.Uid, creds []MsgCredClient, _ []string,
lang string, tmpToken []byte) ([]string, []string, error) {
var validated []string
for i := range creds {
cr := &creds[i]
vld := store.Store.GetValidator(cr.Method)
if vld == nil || !vld.IsInitialized() {
// Ignore unknown or un-initialized validator.
continue
}
if cr.Response != "" {
if _, ok, err := consumeProductVerificationCred(tenantID, uid, cr, vld.PreCheck); ok || err != nil {
if err != nil {
return nil, nil, err
}
validated = append(validated, cr.Method)
continue
}
}
isNew, err := vld.Request(tenantID, uid, cr.Value, lang, cr.Response, tmpToken)
if err != nil {
return nil, nil, err
}
if isNew && cr.Response != "" {
// If response is provided and vld.Request did not return an error, the new request was
// successfully validated.
validated = append(validated, cr.Method)
}
}
return validated, nil, nil
}
func consumeProductVerificationCred(tenantID types.TenantID, uid types.Uid, cr *MsgCredClient,
preCheck func(string, map[string]any) (string, error)) (string, bool, error) {
challengeID, _ := cr.Params["challenge_id"].(string)
challengeID = strings.TrimSpace(challengeID)
if challengeID == "" {
return "", false, nil
}
if store.Verifications == nil {
return "", true, types.ErrInternal
}
normalized, err := preCheck(cr.Value, cr.Params)
if err != nil {
return "", true, err
}
value := strings.TrimPrefix(normalized, cr.Method+":")
vc, err := store.Verifications.Get(tenantID, challengeID)
if err != nil {
return "", true, err
}
if vc.Scene != "register" || vc.State != types.VerificationStateVerified || vc.PhoneHash == "" {
return "", true, types.ErrInvalidResponse
}
tokenHash := hashIMSecret(tenantID, vc.Scene, vc.ChallengeID, vc.PhoneHash, cr.Response)
if vc.VerificationTokenHash == nil || *vc.VerificationTokenHash != tokenHash {
return "", true, types.ErrInvalidResponse
}
now := time.Now().UTC().Round(time.Millisecond)
if !now.Before(vc.ExpiresAt) {
return "", true, types.ErrExpired
}
if err := store.Verifications.Consume(tenantID, vc.ChallengeID, tokenHash, now); err != nil {
return "", true, err
}
_, err = store.Users.UpsertCred(tenantID, &types.Credential{
ObjHeader: types.ObjHeader{CreatedAt: now, UpdatedAt: now},
User: uid.String(),
Method: cr.Method,
Value: value,
Done: true,
})
if err != nil {
return "", true, err
}
return value, true, nil
}
// validatedCreds returns the list of validated credentials including those validated in this call.
// Returns all validated methods including those validated earlier and now.
func validatedCreds(tenantID types.TenantID, uid types.Uid, authLvl auth.Level, creds []MsgCredClient,
errorOnFail bool) ([]string, []string, error) {
// Check if credential validation is required.
if len(globals.authValidators[authLvl]) == 0 {
return nil, nil, nil
}
// Get all validated methods
allCreds, err := store.Users.GetAllCreds(tenantID, uid, "", true)
if err != nil {
return nil, nil, err
}
methods := make(map[string]struct{})
for i := range allCreds {
methods[allCreds[i].Method] = struct{}{}
}
// Add credentials which are validated in this call.
// Unknown validators are removed.
creds = normalizeCredentials(creds, false)
for i := range creds {
cr := &creds[i]
if cr.Response == "" {
// Ignore empty response.
continue
}
vld := store.Store.GetValidator(cr.Method) // No need to check for nil, unknown methods are removed earlier.
_, err := vld.Check(tenantID, uid, cr.Response)
if err != nil {
// Check failed.
if storeErr, ok := err.(types.StoreError); ok && storeErr == types.ErrCredentials {
if errorOnFail {
// Report invalid response.
return nil, nil, types.ErrInvalidResponse
}
// Skip invalid response. Keep credential unvalidated.
continue
}
// Actual error. Report back.
return nil, nil, err
}
// Check did not return an error: the request was successfully validated.
methods[cr.Method] = struct{}{}
}
validated := make([]string, 0, len(methods))
for method := range methods {
validated = append(validated, method)
}
return validated, nil, nil
}
// deleteCred deletes user's credential.
func deleteCred(tenantID types.TenantID, uid types.Uid, authLvl auth.Level, cred *MsgCredClient) ([]string, error) {
vld := store.Store.GetValidator(cred.Method)
if vld == nil || cred.Value == "" {
// Reject invalid request: unknown validation method or missing credential value.
return nil, types.ErrMalformed
}
// Is this a required credential for this validation level?
isRequired := slices.Contains(globals.authValidators[authLvl], cred.Method)
// If credential is required, make sure the method remains validated even after this credential is deleted.
if isRequired {
// There could be multiple validated credentials for the same method thus we are getting a map with count
// for each method.
// Get all credentials of the given method.
allCreds, err := store.Users.GetAllCreds(tenantID, uid, cred.Method, false)
if err != nil {
return nil, err
}
// Check if it's OK to delete: there is another validated value
// or this value is not validated in the first place.
var okTodelete bool
for _, cr := range allCreds {
if (cr.Done && cr.Value != cred.Value) || (!cr.Done && cr.Value == cred.Value) {
okTodelete = true
break
}
}
if !okTodelete {
// Reject: this is the only validated credential and it must be provided.
return nil, types.ErrPolicy
}
}
// The credential is either not required or more than one credential is validated for the given method.
err := vld.Remove(tenantID, uid, cred.Value)
if err != nil {
if err == types.ErrNotFound {
// Credential is not deleted because it's not found
err = nil
}
return nil, err
}
return nil, nil
}
// Change user state: suspended/normal (ok).
// 1. Not needed -- Disable/enable logins (state checked after login).
// 2. If suspending, evict user's sessions. Skip this step if resuming.
// 3. Suspend/activate p2p with the user.
// 4. Suspend/activate grp topics where the user is the owner.
// 5. Update user's DB record.
func changeUserState(s *Session, uid types.Uid, user *types.User, msg *ClientComMessage) (bool, error) {
state, err := types.NewObjState(msg.Acc.State)
if err != nil || state == types.StateUndefined {
logs.Warn.Println("replyUpdateUser: invalid account state", s.sid)
return false, types.ErrMalformed
}
// State unchanged.
if user.State == state {
return false, nil
}
if state != types.StateOK {
// Terminate all sessions.
globals.sessionStore.EvictUser(uid, "")
}
err = store.Users.UpdateState(s.tenantID, uid, state)
if err != nil {
return false, err
}
// Update state of all loaded in memory user's p2p & grp-owner topics.
globals.hub.userStatus <- &userStatusReq{tenantID: s.tenantID, forUser: uid, state: state}
user.State = state
return true, err
}
// Request to delete a user:
// 1. Disable user's login
// 2. Terminate all user's sessions except the current session.
// 3. Stop all active topics
// 4. Notify other subscribers that topics are being deleted.
// 5. Delete user from the database.
// 6. Report success or failure.
// 7. Terminate user's last session.
func replyDelUser(s *Session, msg *ClientComMessage) {
var uid types.Uid
if msg.Del.User == "" || msg.Del.User == s.uid.UserId() {
// Check if account deletion is disabled.
if globals.permanentAccounts && s.authLvl != auth.LevelRoot {
logs.Warn.Println("replyDelUser: account deletion disabled", s.sid)
s.queueOut(ErrPolicy(msg.Id, "", msg.Timestamp))
return
}
// Delete current user.
uid = s.uid
} else if s.authLvl == auth.LevelRoot {
// Delete another user.
uid = types.ParseUserId(msg.Del.User)
if uid.IsZero() {
logs.Warn.Println("replyDelUser: invalid user ID", msg.Del.User, s.sid)
s.queueOut(ErrMalformed(msg.Id, "", msg.Timestamp))
return
}
state, err := userGetState(s.tenantID, uid)
if err != nil || state == types.StateDeleted {
logs.Warn.Println("replyDelUser: target user is outside session tenant", s.sid)
s.queueOut(ErrPermissionDenied(msg.Id, "", msg.Timestamp))
return
}
} else {
logs.Warn.Println("replyDelUser: illegal attempt to delete another user", msg.Del.User, s.sid)
s.queueOut(ErrPermissionDenied(msg.Id, "", msg.Timestamp))
return
}
// Disable all authenticators
authnames := store.Store.GetAuthNames()
for _, name := range authnames {
hdl := store.Store.GetLogicalAuthHandler(name)
if !hdl.IsInitialized() {
continue
}
if err := hdl.DelRecords(s.tenantID, uid); err != nil {
// This could be completely benign, i.e. authenticator exists but not used.
logs.Warn.Println("replyDelUser: failed to delete auth record", uid.UserId(), name, err, s.sid)
if storeErr, ok := err.(types.StoreError); ok && storeErr == types.ErrUnsupported {
// Authenticator refused to delete record: user account cannot be deleted.
s.queueOut(ErrOperationNotAllowed(msg.Id, "", msg.Timestamp))
return
}
}
}
// Terminate all sessions. Skip the current session so the requester gets a response.
globals.sessionStore.EvictUser(uid, s.sid)
// Remove user from cache and announce to cluster that the user is deleted.
usersRemoveUser(msg.TenantID, uid)
// Stop topics where the user is the owner and p2p topics.
done := make(chan bool)
globals.hub.unreg <- &topicUnreg{tenantID: s.tenantID, forUser: uid, del: msg.Del.Hard, done: done}
<-done
// Notify users of interest that the user is gone.
if uoi, err := store.Users.GetSubs(s.tenantID, uid); err == nil {
presUsersOfInterestOffline(s.tenantID, uid, uoi, "gone")
} else {
logs.Warn.Println("replyDelUser: failed to send notifications to users", err, s.sid)
}
// Notify subscribers of the group topics where the user was the owner that the topics were deleted.
if ownTopics, err := store.Users.GetOwnTopics(s.tenantID, uid); err == nil {
for _, topicName := range ownTopics {
if subs, err := store.Topics.GetSubs(s.tenantID, topicName, nil); err == nil {
presSubsOfflineOffline(s.tenantID, topicName, types.TopicCatGrp, subs, "gone", &presParams{}, s.sid)
} else {
logs.Warn.Println("replyDelUser: failed to notify topic subscribers", err, topicName, s.sid)
}
}
} else {
logs.Warn.Println("replyDelUser: failed to send notifications to owned topics", err, s.sid)
}
// TODO: suspend all P2P topics with the user.
// Delete user's records from the database.
if err := store.Users.Delete(s.tenantID, uid, msg.Del.Hard); err != nil {
logs.Warn.Println("replyDelUser: failed to delete user", err, s.sid)
s.queueOut(decodeStoreError(err, msg.Id, msg.Timestamp, nil))
return
}
s.queueOut(NoErr(msg.Id, "", msg.Timestamp))
if s.uid == uid && s.multi == nil {
// Evict the current session if it belongs to the deleted user.
// No need to send it to multiplexing session: remote node will be notified separately.
_, data := s.serialize(NoErrEvicted("", "", msg.Timestamp))
s.stopSession(data)
}
}
// Read user's state from DB.
func userGetState(tenantID types.TenantID, uid types.Uid) (types.ObjState, error) {
user, err := store.Users.Get(tenantID, uid)
if err != nil {
return types.StateUndefined, err
}
if user == nil {
return types.StateUndefined, types.ErrUserNotFound
}
if user.TenantID != tenantID {
return types.StateUndefined, types.ErrUserNotFound
}
return user.State, nil
}
// Subscribe or unsubscribe a single user's device to/from all FCM topics (channels).
func userChannelsSubUnsub(tenantID types.TenantID, uid types.Uid, deviceID string, sub bool) {
push.ChannelSub(&push.ChannelReq{
TenantID: tenantID,
Uid: uid,
DeviceID: deviceID,
Unsub: !sub,
})
}
// UserCacheReq contains data which mutates one or more user cache entries.
type UserCacheReq struct {
TenantID types.TenantID
// Name of the node sending this request in case of cluster. Not set otherwise.
Node string
// UserId is set when count of unread messages is updated for a single user or
// when the user is being deleted.
UserId types.Uid
// UserIdList is set when subscription count is updated for users of a topic.
UserIdList []types.Uid
// Unread count (UserId is set)
Unread int
// In case of set UserId: treat Unread count as an increment as opposite to the final value.
// In case of set UserIdList: intement (Inc == true) or decrement subscription count by one.
Inc bool
// User is being deleted, remove user from cache.
Gone bool
// Optional push notification
PushRcpt *push.Receipt
}
type userCacheEntry struct {
unread int
topics int
}
// Preserved update entry kept while we read the unread counter from the DB.
type bufferedUpdate struct {
val int
inc bool
}
type ioResult struct {
tenantID types.TenantID
counts map[types.Uid]int
err error
}
// Represents pending push notification receipt.
type pendingReceipt struct {
// Number of unread counters currently being read from the DB.
pendingIOs int
// The index is needed by update and is maintained by the heap.Interface methods.
index int
// Underlying receipt.
rcpt *push.Receipt
}
// Pending pushes organized as a priority queue (priority = number of pending IOs).
// It allows to quickly discover receipts ready for sending (num pending IOs is 0).
type pendingReceiptsQueue []*pendingReceipt
// Heap interface methods.
func (pq pendingReceiptsQueue) Len() int { return len(pq) }
func (pq pendingReceiptsQueue) Less(i, j int) bool {
// We want Pop to give us the highest, not lowest, priority so we use greater than here.
return pq[i].pendingIOs < pq[j].pendingIOs
}
func (pq pendingReceiptsQueue) Swap(i, j int) {
pq[i], pq[j] = pq[j], pq[i]
pq[i].index = i
pq[j].index = j
}
func (pq *pendingReceiptsQueue) Push(x any) {
n := len(*pq)
item := x.(*pendingReceipt)
item.index = n
*pq = append(*pq, item)
}
func (pq *pendingReceiptsQueue) Pop() any {
old := *pq
n := len(old)
item := old[n-1]
old[n-1] = nil // avoid memory leak
item.index = -1 // for safety
*pq = old[0 : n-1]
return item
}
func (pq *pendingReceiptsQueue) fix(index int) {
heap.Fix(pq, index)
}
// Initialize users cache.
func usersInit() {
globals.usersUpdate = make(chan *UserCacheReq, 1024)
go userUpdater()
}
// Shutdown users cache.
func usersShutdown() {
if globals.usersUpdate != nil {
globals.usersUpdate <- nil
}
}
func usersUpdateUnread(tenantID types.TenantID, uid types.Uid, val int64, inc bool) {
if globals.usersUpdate == nil || (val == 0 && inc) {
return
}
upd := &UserCacheReq{TenantID: tenantID, UserId: uid, Unread: int(val), Inc: inc}
if globals.cluster.isRemoteTopic(types.TopicKey{TenantID: tenantID, Topic: uid.UserId()}) {
// Send request to remote node which owns the user.
globals.cluster.routeUserReq(upd)
} else {
select {
case globals.usersUpdate <- upd:
default:
}
}
}
// Start tracking a single user. Used for cache management.
// 'add' increments/decrements user's count of subscribed topics.
func usersRegisterUser(tenantID types.TenantID, uid types.Uid, add bool) {
if globals.usersUpdate == nil {
return
}
upd := &UserCacheReq{TenantID: tenantID, UserIdList: make([]types.Uid, 1), Inc: add}
upd.UserIdList[0] = uid
if globals.cluster.isRemoteTopic(types.TopicKey{TenantID: tenantID, Topic: uid.UserId()}) {
// Send request to remote node which owns the user.
globals.cluster.routeUserReq(upd)
} else {
select {
case globals.usersUpdate <- upd:
default:
}
}
}
// Stop tracking user and remove him from cache.
func usersRemoveUser(tenantID types.TenantID, uid types.Uid) {
if globals.usersUpdate == nil {
return
}
upd := &UserCacheReq{TenantID: tenantID, UserId: uid, Gone: true}
if !globals.cluster.isRemoteTopic(types.TopicKey{TenantID: tenantID, Topic: uid.UserId()}) {
select {
case globals.usersUpdate <- upd:
default:
}
}
if globals.cluster != nil {
// Announce to cluster even if the user is local.
globals.cluster.routeUserReq(upd)
}
}
// Account users as members of an active topic. Used for cache management.
// In case of a cluster this method is called only when the topic is local:
// globals.cluster.isRemoteTopic(t.name) == false
func usersRegisterTopic(t *Topic, add bool) {
if globals.usersUpdate == nil {
return
}
if t.cat == types.TopicCatFnd || t.cat == types.TopicCatMe {
// Ignoring me and fnd topics.
return
}
local := &UserCacheReq{TenantID: t.tenantID, Inc: add}
// In case of a cluster UIDs could be local and remote. Process local UIDs locally,
// send remote UIDs to other cluster nodes for processing. The UIDs may have to be
// sent to multiple nodes.
remote := &UserCacheReq{TenantID: t.tenantID, Inc: add}
for uid, pud := range t.perUser {
if pud.isChan {
// Skip channel subscribers.
continue
}
if globals.cluster.isRemoteTopic(types.TopicKey{TenantID: t.tenantID, Topic: uid.UserId()}) {
remote.UserIdList = append(remote.UserIdList, uid)
} else {
local.UserIdList = append(local.UserIdList, uid)
}
}
if len(remote.UserIdList) > 0 {
globals.cluster.routeUserReq(remote)
}
if len(local.UserIdList) > 0 {
select {
case globals.usersUpdate <- local:
default:
logs.Err.Println("User cache: globals.usersUpdate queue full: ", len(globals.usersUpdate))
}
}
}
// usersRequestFromCluster handles requests which came from other cluser nodes.
func usersRequestFromCluster(req *UserCacheReq) {
if globals.usersUpdate == nil {
return
}
select {
case globals.usersUpdate <- req:
default:
}
}
var usersCache map[types.UserKey]userCacheEntry
// The go routine for processing updates to users cache.
func userUpdater() {
// Caches unread counters and numbers of topics the user's subscribed to.
usersCache = make(map[types.UserKey]userCacheEntry)
// Unread counter updates blocked by IO on per user basis. We flush them when the IO completes.
perUserBuffers := make(map[types.UserKey][]bufferedUpdate)
// Push notification recipients blocked by IO (unread counters for some of the recipients
// are being read from the database) on the per user basis.
perUserPendingReceipts := make(map[types.UserKey][]*pendingReceipt)
// All pending push receipts organized as a priority queue by the number of pending IOs.
receiptQueue := pendingReceiptsQueue{}
// IO callback queue.
ioDone := make(chan *ioResult, 1024)
unreadUpdater := func(tenantID types.TenantID, uids []types.Uid, vals []int, inc bool) map[types.Uid]int {
var dbPending []types.Uid
counts := make(map[types.Uid]int, len(uids))
for i, uid := range uids {
key := types.UserKey{TenantID: tenantID, User: uid}
counts[uid] = 0
uce, ok := usersCache[key]
if !ok {
logs.Err.Println("ERROR: attempt to update unread count for user who has not been loaded", uid)
counts[uid] = unreadUpdateError
continue
}
val := vals[i]
if uce.unread < 0 {
// Unread counter not initialized yet. Maybe start a DB read?
if updateBuf, ioInProgress := perUserBuffers[key]; ioInProgress {
// Buffer this update.
updateBuf = append(updateBuf, bufferedUpdate{val: val, inc: inc})
perUserBuffers[key] = updateBuf
} else {
// Schedule reading the counter from DB.
updateBuf = []bufferedUpdate{}
perUserBuffers[key] = updateBuf
dbPending = append(dbPending, uid)
}
counts[uid] = unreadUpdateIOPending
continue
} else if inc {
uce.unread += val
} else {
uce.unread = val
}
usersCache[key] = uce
counts[uid] = uce.unread
store.CacheUserUnreadCount(tenantID, uid, uce.unread)
}
if len(dbPending) > 0 {
go func() {
dbUnread, err := store.Users.GetUnreadCount(tenantID, dbPending...)
if err != nil {
logs.Warn.Println("users: failed to load unread count: ", err)
}
ioDone <- &ioResult{tenantID: tenantID, counts: dbUnread, err: err}
}()
}
return counts
}
for {
select {
case io := <-ioDone:
// Unread counter read has completed.
for uid, count := range io.counts {
key := types.UserKey{TenantID: io.tenantID, User: uid}
updateBuf, ok := perUserBuffers[key]
// Stop buffering updates. New updates will be handled normally.
delete(perUserBuffers, key)
if io.err != nil {
continue
}
// Update counter.
if ok {
for _, upd := range updateBuf {
if upd.inc {
count += upd.val
} else {
count = upd.val
}
}
} else {
logs.Warn.Println("ERROR: io didn't have an update buffer, uid", uid)
}
if uce, ok := usersCache[key]; ok {
if uce.unread >= 0 {
logs.Warn.Println("users: unread count double initialization, uid", uid)
}
uce.unread = count
usersCache[key] = uce
store.CacheUserUnreadCount(io.tenantID, uid, count)
} else {
logs.Warn.Println("users: missing users cache entry after IO completion, uid", uid)
}
// Now that the unread counter is initialized, handle pending push notification receipts.
// Decrease pending IO counts in pending push receipts for this user.
if pendingReceipts, ok := perUserPendingReceipts[key]; ok {
for _, pp := range pendingReceipts {
pp.pendingIOs--
receiptQueue.fix(pp.index)
}
delete(perUserPendingReceipts, key)
}
}
if io.err != nil {
logs.Err.Println("users: failed to read unread count:", io.err)
continue
}
// Send ready receipts.
for receiptQueue.Len() > 0 && receiptQueue[0].pendingIOs == 0 {
rcpt := heap.Pop(&receiptQueue).(*pendingReceipt).rcpt
for uid, rcptTo := range rcpt.To {
key := types.UserKey{TenantID: rcpt.TenantID, User: uid}
if uce, ok := usersCache[key]; ok && uce.unread >= 0 {
rcptTo.Unread = uce.unread
rcpt.To[uid] = rcptTo
}
}
push.Push(rcpt)
}
case upd := <-globals.usersUpdate:
if globals.shuttingDown {
// If shutdown is in progress we don't care to process anything.
// ignore all calls.
continue
}
// Shutdown requested.
if upd == nil {
globals.usersUpdate = nil
// Dont' care to close the channel.
goto Exit
}
// Request to send push notifications.
if upd.PushRcpt != nil {
// List of uids for which the unread count is being read from the DB.
pendingUsers := []types.Uid{}
allUids := make([]types.Uid, 0, len(upd.PushRcpt.To))
allDeltas := make([]int, 0, len(upd.PushRcpt.To))
for uid, r := range upd.PushRcpt.To {
allUids = append(allUids, uid)
delta := 0
if r.ShouldIncrementUnreadCountInCache {
delta = 1
}
allDeltas = append(allDeltas, delta)
}
allUnread := unreadUpdater(upd.TenantID, allUids, allDeltas, true)
for uid, unread := range allUnread {
rcptTo := upd.PushRcpt.To[uid]
// Handle update
if unread >= 0 {
rcptTo.Unread = unread
upd.PushRcpt.To[uid] = rcptTo
} else if unread == unreadUpdateIOPending {
pendingUsers = append(pendingUsers, uid)
}
}
if len(pendingUsers) == 0 {
// All data present in memory. Just send the push.
push.Push(upd.PushRcpt)
} else {
// We are waiting for IO. Add this receipt to the queues.
pp := &pendingReceipt{
pendingIOs: len(pendingUsers),
rcpt: upd.PushRcpt,
}
for _, uid := range pendingUsers {
key := types.UserKey{TenantID: upd.TenantID, User: uid}
var queue []*pendingReceipt
var ok bool
if queue, ok = perUserPendingReceipts[key]; !ok {
queue = []*pendingReceipt{}
}
perUserPendingReceipts[key] = append(queue, pp)
}
heap.Push(&receiptQueue, pp)
}
continue
}
// Request to add/remove user from cache.
if len(upd.UserIdList) > 0 {
for _, uid := range upd.UserIdList {
key := types.UserKey{TenantID: upd.TenantID, User: uid}
uce, ok := usersCache[key]
if upd.Inc {
if !ok {
// This is a registration of a new user.
// We are not loading unread count here, so set it to -1.
uce.unread = -1
}
uce.topics++
usersCache[key] = uce
} else if ok {
if uce.topics > 1 {
uce.topics--
usersCache[key] = uce
} else {
// Remove user from cache
delete(usersCache, key)
}
} else {
// BUG!
logs.Err.Println("ERROR: request to unregister user which has not been registered", uid)
}
}
continue
}
if upd.Gone {
// User is being deleted. Don't care if there is a record.
delete(usersCache, types.UserKey{TenantID: upd.TenantID, User: upd.UserId})
store.DeleteCachedUserUnreadCount(upd.TenantID, upd.UserId)
continue
}
// Request to update unread count for one user.
unreadUpdater(upd.TenantID, []types.Uid{upd.UserId}, []int{upd.Unread}, upd.Inc)
}
}
Exit:
logs.Info.Println("users: shutdown")
}
// garbageCollectUsers runs every 'period' and deletes up to 'blockSize'
// stale unvalidated user accounts which have been last updated at least
// 'minAccountAgeHours' hours.
// Returns channel which can be used to stop the process.
func garbageCollectUsers(period time.Duration, blockSize, minAccountAgeHours int) chan<- bool {
// Unbuffered stop channel. Whomever stops the gc must wait for the process to finish.
stop := make(chan bool)
go func() {
// Add some randomness to the tick period to desynchronize runs on cluster nodes:
// 0.75 * period + rand(0, 0.5) * period.
period = period - (period >> 2) + time.Duration(rand.Intn(int(period>>1)))
gcTicker := time.Tick(period)
logs.Info.Printf("Stale account GC started with period %s, block size %d, min account age %d hours",
period.Round(time.Second), blockSize, minAccountAgeHours)
staleAge := time.Hour * time.Duration(minAccountAgeHours)
for {
select {
case <-gcTicker:
for _, tenantID := range store.KnownTenantIDs() {
if uids, err := store.Users.GetUnvalidated(tenantID, time.Now().Add(-staleAge), blockSize); err == nil {
if len(uids) > 0 {
logs.Info.Println("Stale account GC will delete uids:", tenantID, uids)
for _, uid := range uids {
if err = store.Users.Delete(tenantID, uid, true); err != nil {
logs.Warn.Printf("Stale account GC failed to delete %s: %+v", uid.UserId(), err)
}
}
}
} else {
logs.Warn.Println("Stale account GC error:", tenantID, err)
}
}
case <-stop:
return
}
}
}()
return stop
}