github.com/decred/politeia@v1.4.0/politeiawww/legacy/cmsuser.go (about)

     1  // Copyright (c) 2017-2020 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package legacy
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/hex"
    10  	"errors"
    11  	"fmt"
    12  	"sort"
    13  	"strings"
    14  	"time"
    15  
    16  	cms "github.com/decred/politeia/politeiawww/api/cms/v1"
    17  	www "github.com/decred/politeia/politeiawww/api/www/v1"
    18  	"github.com/decred/politeia/politeiawww/legacy/user"
    19  	"github.com/google/uuid"
    20  )
    21  
    22  // processInviteNewUser creates a new user in the db if it doesn't already
    23  // exist and sets a verification token and expiry; the token must be
    24  // verified before it expires. If the user already exists in the db
    25  // and its token is expired, it generates a new one.
    26  //
    27  // Note that this function always returns a InviteNewUserReply. The caller
    28  // shall verify error and determine how to return this information upstream.
    29  func (p *Politeiawww) processInviteNewUser(u cms.InviteNewUser) (*cms.InviteNewUserReply, error) {
    30  	log.Tracef("processInviteNewUser: %v", u.Email)
    31  
    32  	// Validate email
    33  	if !validEmail.MatchString(u.Email) {
    34  		log.Debugf("processInviteNewUser: invalid email '%v'", u.Email)
    35  		return nil, www.UserError{
    36  			ErrorCode: www.ErrorStatusMalformedEmail,
    37  		}
    38  	}
    39  
    40  	// Check if the user is already verified.
    41  	existingUser, err := p.userByEmail(u.Email)
    42  	if err == nil {
    43  		if existingUser.NewUserVerificationToken == nil {
    44  			return &cms.InviteNewUserReply{}, nil
    45  		}
    46  	}
    47  
    48  	// Generate the verification token and expiry.
    49  	token, expiry, err := newVerificationTokenAndExpiry()
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  
    54  	// Try to email the verification link first; if it fails, then
    55  	// the new user won't be created.
    56  	//
    57  	// This is conditional on the email server being setup.
    58  	err = p.emailUserCMSInvite(u.Email, hex.EncodeToString(token))
    59  	if err != nil {
    60  		log.Errorf("processInviteNewUser: verification email "+
    61  			"failed for '%v': %v", u.Email, err)
    62  		return &cms.InviteNewUserReply{}, nil
    63  	}
    64  
    65  	// If the user already exists, the user record is updated
    66  	// in the db in order to reset the verification token and
    67  	// expiry.
    68  	if existingUser != nil {
    69  		existingUser.NewUserVerificationToken = token
    70  		existingUser.NewUserVerificationExpiry = expiry
    71  		err = p.db.UserUpdate(*existingUser)
    72  		if err != nil {
    73  			return nil, err
    74  		}
    75  
    76  		return &cms.InviteNewUserReply{
    77  			VerificationToken: hex.EncodeToString(token),
    78  		}, nil
    79  	}
    80  
    81  	// Create a new cms user with the provided information.
    82  	// This temporarily sets the username to the given email, which will be
    83  	// overwritten during registration. This is needed since the constraints
    84  	// on cockroachdb for usernames requires there to be no duplicates. If
    85  	// unset, the username of "" will cause a duplicate error to be thrown.
    86  	nu := user.NewCMSUser{
    87  		Email:                     strings.ToLower(u.Email),
    88  		Username:                  strings.ToLower(u.Email),
    89  		NewUserVerificationToken:  token,
    90  		NewUserVerificationExpiry: expiry,
    91  	}
    92  
    93  	// Set the user to temporary if request includes it.
    94  	// While this will allow a user to bypass the DCC process, the temporary
    95  	// users will have extensive restrictions on their account and ability
    96  	// to submit invoices.
    97  	if u.Temporary {
    98  		nu.ContractorType = int(cms.ContractorTypeTemp)
    99  	} else {
   100  		nu.ContractorType = int(cms.ContractorTypeNominee)
   101  	}
   102  
   103  	payload, err := user.EncodeNewCMSUser(nu)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  	pc := user.PluginCommand{
   108  		ID:      user.CMSPluginID,
   109  		Command: user.CmdNewCMSUser,
   110  		Payload: string(payload),
   111  	}
   112  	_, err = p.db.PluginExec(pc)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	// Update user emails cache
   118  	usr, err := p.db.UserGetByUsername(nu.Username)
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  	p.setUserEmailsCache(usr.Email, usr.ID)
   123  
   124  	return &cms.InviteNewUserReply{
   125  		VerificationToken: hex.EncodeToString(token),
   126  	}, nil
   127  }
   128  
   129  // processRegisterUser allows a CMS user that has received an invite to
   130  // register their account. The username and password for the account are
   131  // updated and the user's email address and identity are marked as verified.
   132  func (p *Politeiawww) processRegisterUser(u cms.RegisterUser) (*cms.RegisterUserReply, error) {
   133  	log.Tracef("processRegisterUser: %v", u.Email)
   134  	var reply cms.RegisterUserReply
   135  
   136  	// Check that the user already exists.
   137  	existingUser, err := p.userByEmail(u.Email)
   138  	if err != nil {
   139  		if errors.Is(err, user.ErrUserNotFound) {
   140  			log.Debugf("RegisterUser failure for %v: user not found",
   141  				u.Email)
   142  			return nil, www.UserError{
   143  				ErrorCode: www.ErrorStatusVerificationTokenInvalid,
   144  			}
   145  		}
   146  		return nil, err
   147  	}
   148  
   149  	// Validate public key and ensure its unique.
   150  	err = validatePubKey(u.PublicKey)
   151  	if err != nil {
   152  		return nil, err
   153  	}
   154  	_, err = p.db.UserGetByPubKey(u.PublicKey)
   155  	switch err {
   156  	case user.ErrUserNotFound:
   157  		// Pubkey is unique; continue
   158  	case nil:
   159  		// Pubkey is not unique
   160  		return nil, www.UserError{
   161  			ErrorCode: www.ErrorStatusDuplicatePublicKey,
   162  		}
   163  	default:
   164  		// All other errors
   165  		return nil, err
   166  	}
   167  
   168  	// Format and validate the username.
   169  	username := formatUsername(u.Username)
   170  	err = validateUsername(username)
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  
   175  	// Ensure username is unique.
   176  	_, err = p.db.UserGetByUsername(u.Username)
   177  	switch err {
   178  	case nil:
   179  		// Duplicate username
   180  		return nil, www.UserError{
   181  			ErrorCode: www.ErrorStatusDuplicateUsername,
   182  		}
   183  	case user.ErrUserNotFound:
   184  		// Username doesn't exist; continue
   185  	default:
   186  		return nil, err
   187  	}
   188  
   189  	// Validate the password.
   190  	err = validatePassword(u.Password)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  
   195  	// Hash the user's password.
   196  	hashedPassword, err := p.hashPassword(u.Password)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  
   201  	// Decode the verification token.
   202  	token, err := hex.DecodeString(u.VerificationToken)
   203  	if err != nil {
   204  		log.Debugf("Register failure for %v: verification token could "+
   205  			"not be decoded: %v", u.Email, err)
   206  		return nil, www.UserError{
   207  			ErrorCode: www.ErrorStatusVerificationTokenInvalid,
   208  		}
   209  	}
   210  
   211  	// Check that the verification token matches.
   212  	if !bytes.Equal(token, existingUser.NewUserVerificationToken) {
   213  		log.Debugf("Register failure for %v: verification token doesn't "+
   214  			"match, expected %v", u.Email, existingUser.NewUserVerificationToken)
   215  		return nil, www.UserError{
   216  			ErrorCode: www.ErrorStatusVerificationTokenInvalid,
   217  		}
   218  	}
   219  
   220  	// Check that the token hasn't expired.
   221  	if time.Now().Unix() > existingUser.NewUserVerificationExpiry {
   222  		log.Debugf("Register failure for %v: verification token expired",
   223  			u.Email)
   224  		return nil, www.UserError{
   225  			ErrorCode: www.ErrorStatusVerificationTokenExpired,
   226  		}
   227  	}
   228  
   229  	// Create a new database user with the provided information.
   230  	newUser := user.User{
   231  		ID:                        existingUser.ID,
   232  		Email:                     strings.ToLower(u.Email),
   233  		Username:                  username,
   234  		HashedPassword:            hashedPassword,
   235  		NewUserVerificationToken:  nil,
   236  		NewUserVerificationExpiry: 0,
   237  	}
   238  
   239  	// Setup newUser's identity with the provided public key. An
   240  	// additional verification step to activate the identity is
   241  	// not needed since the registration email already serves as
   242  	// the verification.
   243  	id, err := user.NewIdentity(u.PublicKey)
   244  	if err != nil {
   245  		return nil, err
   246  	}
   247  	err = newUser.AddIdentity(*id)
   248  	if err != nil {
   249  		return nil, err
   250  	}
   251  	err = newUser.ActivateIdentity(id.Key[:])
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  
   256  	// Update the user in the db.
   257  	err = p.db.UserUpdate(newUser)
   258  	if err != nil {
   259  		return nil, err
   260  	}
   261  
   262  	// Even if user is non-nil, this will bring it up-to-date
   263  	// with the new information inserted via newUser.
   264  	existingUser, err = p.userByEmail(newUser.Email)
   265  	if err != nil {
   266  		return nil, fmt.Errorf("unable to retrieve account info for %v: %v",
   267  			newUser.Email, err)
   268  	}
   269  
   270  	err = p.db.UserUpdate(newUser)
   271  	if err != nil {
   272  		return nil, err
   273  	}
   274  	return &reply, nil
   275  }
   276  
   277  // processCMSUsers returns a list of cms users given a set of filters. If
   278  // either domain or contractor is non-zero then they are used as matching
   279  // criteria, otherwise the full list will be returned.
   280  func (p *Politeiawww) processCMSUsers(users *cms.CMSUsers) (*cms.CMSUsersReply, error) {
   281  	log.Tracef("processCMSUsers")
   282  
   283  	domain := int(users.Domain)
   284  	contractortype := int(users.ContractorType)
   285  
   286  	matchedUsers := make([]cms.AbridgedCMSUser, 0, www.UserListPageSize)
   287  
   288  	if domain != 0 {
   289  		// Setup plugin command
   290  		cu := user.CMSUsersByDomain{
   291  			Domain: domain,
   292  		}
   293  		payload, err := user.EncodeCMSUsersByDomain(cu)
   294  		if err != nil {
   295  			return nil, err
   296  		}
   297  		pc := user.PluginCommand{
   298  			ID:      user.CMSPluginID,
   299  			Command: user.CmdCMSUsersByDomain,
   300  			Payload: string(payload),
   301  		}
   302  
   303  		// Execute plugin command
   304  		pcr, err := p.db.PluginExec(pc)
   305  		if err != nil {
   306  			return nil, err
   307  		}
   308  
   309  		// Decode reply
   310  		reply, err := user.DecodeCMSUsersByDomainReply([]byte(pcr.Payload))
   311  		if err != nil {
   312  			return nil, err
   313  		}
   314  		for _, u := range reply.Users {
   315  			// Only add matched users if contractor type is 0  or it matches
   316  			// the request
   317  			if contractortype == 0 ||
   318  				(contractortype != 0 && u.ContractorType == contractortype) {
   319  				matchedUsers = append(matchedUsers, cms.AbridgedCMSUser{
   320  					Domain:         cms.DomainTypeT(u.Domain),
   321  					Username:       u.Username,
   322  					ContractorType: cms.ContractorTypeT(u.ContractorType),
   323  					ID:             u.ID.String(),
   324  				})
   325  			}
   326  		}
   327  	} else if contractortype != 0 {
   328  		// Setup plugin command
   329  		cu := user.CMSUsersByContractorType{
   330  			ContractorType: contractortype,
   331  		}
   332  		payload, err := user.EncodeCMSUsersByContractorType(cu)
   333  		if err != nil {
   334  			return nil, err
   335  		}
   336  		pc := user.PluginCommand{
   337  			ID:      user.CMSPluginID,
   338  			Command: user.CmdCMSUsersByContractorType,
   339  			Payload: string(payload),
   340  		}
   341  
   342  		// Execute plugin command
   343  		pcr, err := p.db.PluginExec(pc)
   344  		if err != nil {
   345  			return nil, err
   346  		}
   347  
   348  		// Decode reply
   349  		reply, err := user.DecodeCMSUsersByContractorTypeReply(
   350  			[]byte(pcr.Payload))
   351  		if err != nil {
   352  			return nil, err
   353  		}
   354  		for _, u := range reply.Users {
   355  			// We already know domain is 0 if here so no need to check.
   356  			matchedUsers = append(matchedUsers, cms.AbridgedCMSUser{
   357  				Domain:         cms.DomainTypeT(u.Domain),
   358  				Username:       u.Username,
   359  				ContractorType: cms.ContractorTypeT(u.ContractorType),
   360  				ID:             u.ID.String(),
   361  			})
   362  		}
   363  	} else {
   364  		// Both contractor type and domain are 0 so just return all users.
   365  		err := p.db.AllUsers(func(u *user.User) {
   366  			// Setup plugin command
   367  			cu := user.CMSUserByID{
   368  				ID: u.ID.String(),
   369  			}
   370  			payload, err := user.EncodeCMSUserByID(cu)
   371  			if err != nil {
   372  				log.Error(err)
   373  				return
   374  			}
   375  			pc := user.PluginCommand{
   376  				ID:      user.CMSPluginID,
   377  				Command: user.CmdCMSUserByID,
   378  				Payload: string(payload),
   379  			}
   380  
   381  			// Execute plugin command
   382  			pcr, err := p.db.PluginExec(pc)
   383  			if err != nil {
   384  				log.Error(err)
   385  				return
   386  			}
   387  
   388  			// Decode reply
   389  			reply, err := user.DecodeCMSUserByIDReply([]byte(pcr.Payload))
   390  			if err != nil {
   391  				log.Error(err)
   392  				return
   393  			}
   394  			matchedUsers = append(matchedUsers, cms.AbridgedCMSUser{
   395  				ID:             u.ID.String(),
   396  				Username:       u.Username,
   397  				Domain:         cms.DomainTypeT(reply.User.Domain),
   398  				ContractorType: cms.ContractorTypeT(reply.User.ContractorType),
   399  			})
   400  		})
   401  		if err != nil {
   402  			return nil, err
   403  		}
   404  	}
   405  
   406  	// Sort results alphabetically.
   407  	sort.Slice(matchedUsers, func(i, j int) bool {
   408  		return matchedUsers[i].Username < matchedUsers[j].Username
   409  	})
   410  
   411  	return &cms.CMSUsersReply{
   412  		Users: matchedUsers,
   413  	}, nil
   414  }
   415  
   416  func (p *Politeiawww) processEditCMSUser(ecu cms.EditUser, u *user.User) (*cms.EditUserReply, error) {
   417  	log.Tracef("processEditCMSUser: %v", u.Email)
   418  
   419  	reply := cms.EditUserReply{}
   420  
   421  	err := validateUserInformation(ecu)
   422  	if err != nil {
   423  		return nil, err
   424  	}
   425  
   426  	uu := user.UpdateCMSUser{
   427  		ID: u.ID,
   428  	}
   429  
   430  	if ecu.GitHubName != "" {
   431  		uu.GitHubName = ecu.GitHubName
   432  	}
   433  	if ecu.MatrixName != "" {
   434  		uu.MatrixName = ecu.MatrixName
   435  	}
   436  	if ecu.ContractorName != "" {
   437  		uu.ContractorName = ecu.ContractorName
   438  	}
   439  	if ecu.ContractorLocation != "" {
   440  		uu.ContractorLocation = ecu.ContractorLocation
   441  	}
   442  	if ecu.ContractorContact != "" {
   443  		uu.ContractorContact = ecu.ContractorContact
   444  	}
   445  	payload, err := user.EncodeUpdateCMSUser(uu)
   446  	if err != nil {
   447  		return nil, err
   448  	}
   449  	pc := user.PluginCommand{
   450  		ID:      user.CMSPluginID,
   451  		Command: user.CmdUpdateCMSUser,
   452  		Payload: string(payload),
   453  	}
   454  	_, err = p.db.PluginExec(pc)
   455  	if err != nil {
   456  		return nil, err
   457  	}
   458  	return &reply, nil
   459  }
   460  
   461  func (p *Politeiawww) processManageCMSUser(mu cms.CMSManageUser) (*cms.CMSManageUserReply, error) {
   462  	log.Tracef("processManageCMSUser: %v", mu.UserID)
   463  
   464  	editUser, err := p.userByIDStr(mu.UserID)
   465  	if err != nil {
   466  		return nil, err
   467  	}
   468  
   469  	uu := user.UpdateCMSUser{
   470  		ID: editUser.ID,
   471  	}
   472  
   473  	if mu.Domain != 0 {
   474  		uu.Domain = int(mu.Domain)
   475  	}
   476  	if mu.ContractorType != 0 {
   477  		uu.ContractorType = int(mu.ContractorType)
   478  	}
   479  	if len(mu.SupervisorUserIDs) > 0 {
   480  		// Validate SupervisorUserID input
   481  		parseSuperUserIds := make([]uuid.UUID, 0, len(mu.SupervisorUserIDs))
   482  		for _, super := range mu.SupervisorUserIDs {
   483  			parseUUID, err := uuid.Parse(super)
   484  			if err != nil {
   485  				e := fmt.Sprintf("invalid uuid: %v", super)
   486  				return nil, www.UserError{
   487  					ErrorCode:    cms.ErrorStatusInvalidSupervisorUser,
   488  					ErrorContext: []string{e},
   489  				}
   490  			}
   491  			u, err := p.getCMSUserByID(super)
   492  			if err != nil {
   493  				e := fmt.Sprintf("user not found: %v", super)
   494  				return nil, www.UserError{
   495  					ErrorCode:    cms.ErrorStatusInvalidSupervisorUser,
   496  					ErrorContext: []string{e},
   497  				}
   498  			}
   499  			if u.ContractorType != cms.ContractorTypeSupervisor {
   500  				e := fmt.Sprintf("user not a supervisor: %v", super)
   501  				return nil, www.UserError{
   502  					ErrorCode:    cms.ErrorStatusInvalidSupervisorUser,
   503  					ErrorContext: []string{e},
   504  				}
   505  			}
   506  			parseSuperUserIds = append(parseSuperUserIds, parseUUID)
   507  		}
   508  		uu.SupervisorUserIDs = parseSuperUserIds
   509  	}
   510  
   511  	if len(mu.ProposalsOwned) > 0 {
   512  		uu.ProposalsOwned = mu.ProposalsOwned
   513  	}
   514  
   515  	payload, err := user.EncodeUpdateCMSUser(uu)
   516  	if err != nil {
   517  		return nil, err
   518  	}
   519  	pc := user.PluginCommand{
   520  		ID:      user.CMSPluginID,
   521  		Command: user.CmdUpdateCMSUser,
   522  		Payload: string(payload),
   523  	}
   524  	_, err = p.db.PluginExec(pc)
   525  	if err != nil {
   526  		return nil, err
   527  	}
   528  	return &cms.CMSManageUserReply{}, nil
   529  }
   530  
   531  func (p *Politeiawww) processCMSUserDetails(ud *cms.UserDetails, isCurrentUser bool, isAdmin bool) (*cms.UserDetailsReply, error) {
   532  	reply := cms.UserDetailsReply{}
   533  	u, err := p.getCMSUserByID(ud.UserID)
   534  	if err != nil {
   535  		return nil, err
   536  	}
   537  
   538  	// Filter returned fields in case the user isn't the admin or the current user
   539  	if !isAdmin && !isCurrentUser {
   540  		reply.User = filterCMSUserPublicFields(*u)
   541  	} else {
   542  		reply.User = *u
   543  	}
   544  
   545  	return &reply, nil
   546  }
   547  
   548  // cmsUsersByDomain returns all cms user within the provided contractor domain.
   549  func (p *Politeiawww) cmsUsersByDomain(d cms.DomainTypeT) ([]user.CMSUser, error) {
   550  	// Setup plugin command
   551  	cu := user.CMSUsersByDomain{
   552  		Domain: int(d),
   553  	}
   554  	payload, err := user.EncodeCMSUsersByDomain(cu)
   555  	if err != nil {
   556  		return nil, err
   557  	}
   558  	pc := user.PluginCommand{
   559  		ID:      user.CMSPluginID,
   560  		Command: user.CmdCMSUsersByDomain,
   561  		Payload: string(payload),
   562  	}
   563  
   564  	// Execute plugin command
   565  	pcr, err := p.db.PluginExec(pc)
   566  	if err != nil {
   567  		return nil, err
   568  	}
   569  
   570  	// Decode reply
   571  	reply, err := user.DecodeCMSUsersByDomainReply([]byte(pcr.Payload))
   572  	if err != nil {
   573  		return nil, err
   574  	}
   575  
   576  	return reply.Users, nil
   577  }
   578  
   579  // filterCMSUserPublicFields creates a filtered copy of a cms User that only
   580  // contains public information.
   581  func filterCMSUserPublicFields(user cms.User) cms.User {
   582  	return cms.User{
   583  		ID:         user.ID,
   584  		Admin:      user.Admin,
   585  		Username:   user.Username,
   586  		Identities: user.Identities,
   587  
   588  		// CMS User Details
   589  		ContractorType: user.ContractorType,
   590  		GitHubName:     user.GitHubName,
   591  		MatrixName:     user.MatrixName,
   592  		Domain:         user.Domain,
   593  	}
   594  }
   595  
   596  func validateUserInformation(userInfo cms.EditUser) error {
   597  	var err error
   598  	if userInfo.GitHubName != "" {
   599  		contact := formatContact(userInfo.GitHubName)
   600  		err = validateContact(contact)
   601  		if err != nil {
   602  			return www.UserError{
   603  				ErrorCode: cms.ErrorStatusInvoiceMalformedContact,
   604  			}
   605  		}
   606  	}
   607  	if userInfo.MatrixName != "" {
   608  		contact := formatContact(userInfo.MatrixName)
   609  		err = validateContact(contact)
   610  		if err != nil {
   611  			return www.UserError{
   612  				ErrorCode: cms.ErrorStatusInvoiceMalformedContact,
   613  			}
   614  		}
   615  	}
   616  	if userInfo.ContractorName != "" {
   617  		name := formatName(userInfo.ContractorName)
   618  		err = validateName(name)
   619  		if err != nil {
   620  			return www.UserError{
   621  				ErrorCode: cms.ErrorStatusMalformedName,
   622  			}
   623  		}
   624  	}
   625  	if userInfo.ContractorLocation != "" {
   626  		location := formatLocation(userInfo.ContractorLocation)
   627  		err = validateLocation(location)
   628  		if err != nil {
   629  			return www.UserError{
   630  				ErrorCode: cms.ErrorStatusMalformedLocation,
   631  			}
   632  		}
   633  	}
   634  	if userInfo.ContractorContact != "" {
   635  		contact := formatContact(userInfo.ContractorContact)
   636  		err = validateContact(contact)
   637  		if err != nil {
   638  			return www.UserError{
   639  				ErrorCode: cms.ErrorStatusInvoiceMalformedContact,
   640  			}
   641  		}
   642  	}
   643  	return nil
   644  }
   645  func (p *Politeiawww) getCMSUserByID(id string) (*cms.User, error) {
   646  	ubi := user.CMSUserByID{
   647  		ID: id,
   648  	}
   649  	payload, err := user.EncodeCMSUserByID(ubi)
   650  	if err != nil {
   651  		return nil, err
   652  	}
   653  	pc := user.PluginCommand{
   654  		ID:      user.CMSPluginID,
   655  		Command: user.CmdCMSUserByID,
   656  		Payload: string(payload),
   657  	}
   658  	payloadReply, err := p.db.PluginExec(pc)
   659  	if err != nil {
   660  		return nil, err
   661  	}
   662  	ubir, err := user.DecodeCMSUserByIDReply([]byte(payloadReply.Payload))
   663  	if err != nil {
   664  		return nil, err
   665  	}
   666  	u := convertCMSUserFromDatabaseUser(ubir.User)
   667  	return &u, nil
   668  }
   669  
   670  func (p *Politeiawww) getCMSUserByIDRaw(id string) (*user.CMSUser, error) {
   671  	ubi := user.CMSUserByID{
   672  		ID: id,
   673  	}
   674  	payload, err := user.EncodeCMSUserByID(ubi)
   675  	if err != nil {
   676  		return nil, err
   677  	}
   678  	pc := user.PluginCommand{
   679  		ID:      user.CMSPluginID,
   680  		Command: user.CmdCMSUserByID,
   681  		Payload: string(payload),
   682  	}
   683  	payloadReply, err := p.db.PluginExec(pc)
   684  	if err != nil {
   685  		return nil, err
   686  	}
   687  	ubir, err := user.DecodeCMSUserByIDReply([]byte(payloadReply.Payload))
   688  	if err != nil {
   689  		return nil, err
   690  	}
   691  	return ubir.User, nil
   692  }
   693  
   694  func (p *Politeiawww) getCMSUserWeights() (map[string]int64, error) {
   695  	userWeights := make(map[string]int64, 1080)
   696  
   697  	/*
   698  		1) Determine most recent payout month
   699  		2) For each user
   700  		   1) Look back for 6 months of invoices (that were paid out)
   701  		   2) Add up all minutes billed over that time period.
   702  		   3) Set user weight as total number of billed minutes.
   703  	*/
   704  
   705  	weightEnd := time.Now()
   706  	weightMonthEnd := uint(weightEnd.Month())
   707  	weightYearEnd := uint(weightEnd.Year())
   708  
   709  	weightStart := time.Now().AddDate(0, -1*userWeightMonthLookback, 0)
   710  	weightMonthStart := uint(weightStart.Month())
   711  	weightYearStart := uint(weightStart.Year())
   712  
   713  	// Subtract one nano second from start date and add one to end date to avoid having equal times.
   714  	startDate := time.Date(int(weightYearStart), time.Month(weightMonthStart), 0, 0, 0, 0, -1, time.UTC)
   715  	endDate := time.Date(int(weightYearEnd), time.Month(weightMonthEnd), 0, 0, 0, 0, 1, time.UTC)
   716  
   717  	err := p.db.AllUsers(func(user *user.User) {
   718  		cmsUser, err := p.getCMSUserByID(user.ID.String())
   719  		if err != nil {
   720  			log.Errorf("getCMSUserWeights: getCMSUserByID %v %v",
   721  				user.ID.String(), err)
   722  			return
   723  		}
   724  
   725  		if cmsUser.ContractorType != cms.ContractorTypeDirect &&
   726  			cmsUser.ContractorType != cms.ContractorTypeSubContractor &&
   727  			cmsUser.ContractorType != cms.ContractorTypeSupervisor {
   728  			return
   729  		}
   730  
   731  		var billedMinutes int64
   732  		// Calculate sub contractor weight here
   733  		if cmsUser.ContractorType == cms.ContractorTypeSubContractor {
   734  			for _, superID := range cmsUser.SupervisorUserIDs {
   735  				superUser, err := p.getCMSUserByID(superID)
   736  				if err != nil {
   737  					log.Errorf("getCMSUserWeights: getCMSUserByID %v %v",
   738  						superID, err)
   739  					return
   740  				}
   741  				superInvoices, err := p.cmsDB.InvoicesByUserID(superUser.ID)
   742  				if err != nil {
   743  					log.Errorf("getCMSUserWeights: InvoicesByUserID %v", err)
   744  				}
   745  				for _, i := range superInvoices {
   746  					invoiceDate := time.Date(int(i.Year), time.Month(i.Month), 0, 0, 0, 0, 0, time.UTC)
   747  					if invoiceDate.After(startDate) && endDate.After(invoiceDate) {
   748  						for _, li := range i.LineItems {
   749  							// Only take into account billed minutes if the line
   750  							// item matches their userID
   751  							if li.Type == cms.LineItemTypeSubHours &&
   752  								li.SubUserID == user.ID.String() {
   753  								billedMinutes += int64(li.Labor)
   754  							}
   755  						}
   756  					}
   757  				}
   758  			}
   759  		} else {
   760  			userInvoices, err := p.cmsDB.InvoicesByUserID(cmsUser.ID)
   761  			if err != nil {
   762  				log.Errorf("getCMSUserWeights: InvoicesByUserID %v", err)
   763  				return
   764  			}
   765  			for _, i := range userInvoices {
   766  				invoiceDate := time.Date(int(i.Year), time.Month(i.Month), 0, 0, 0, 0, 0, time.UTC)
   767  				if invoiceDate.After(startDate) && endDate.After(invoiceDate) {
   768  					// now look at the lineitems within that invoice and
   769  					// tabulate billed hours
   770  					for _, li := range i.LineItems {
   771  						billedMinutes += int64(li.Labor)
   772  					}
   773  				}
   774  			}
   775  		}
   776  		userWeights[cmsUser.ID] = billedMinutes
   777  	})
   778  	if err != nil {
   779  		return nil, err
   780  	}
   781  
   782  	return userWeights, nil
   783  
   784  }
   785  
   786  // convertCMSUserFromDatabaseUser converts a user User to a cms User.
   787  func convertCMSUserFromDatabaseUser(user *user.CMSUser) cms.User {
   788  	superUserIDs := make([]string, 0, len(user.SupervisorUserIDs))
   789  	for _, userIDs := range user.SupervisorUserIDs {
   790  		superUserIDs = append(superUserIDs, userIDs.String())
   791  	}
   792  	return cms.User{
   793  		ID:                              user.User.ID.String(),
   794  		Admin:                           user.User.Admin,
   795  		Email:                           user.User.Email,
   796  		Username:                        user.User.Username,
   797  		NewUserVerificationToken:        user.User.NewUserVerificationToken,
   798  		NewUserVerificationExpiry:       user.User.NewUserVerificationExpiry,
   799  		UpdateKeyVerificationToken:      user.User.UpdateKeyVerificationToken,
   800  		UpdateKeyVerificationExpiry:     user.User.UpdateKeyVerificationExpiry,
   801  		ResetPasswordVerificationToken:  user.User.ResetPasswordVerificationToken,
   802  		ResetPasswordVerificationExpiry: user.User.ResetPasswordVerificationExpiry,
   803  		LastLoginTime:                   user.User.LastLoginTime,
   804  		FailedLoginAttempts:             user.User.FailedLoginAttempts,
   805  		Deactivated:                     user.User.Deactivated,
   806  		Locked:                          userIsLocked(user.User.FailedLoginAttempts),
   807  		Identities:                      convertWWWIdentitiesFromDatabaseIdentities(user.User.Identities),
   808  		EmailNotifications:              user.User.EmailNotifications,
   809  		Domain:                          cms.DomainTypeT(user.Domain),
   810  		ContractorType:                  cms.ContractorTypeT(user.ContractorType),
   811  		ContractorName:                  user.ContractorName,
   812  		ContractorLocation:              user.ContractorLocation,
   813  		ContractorContact:               user.ContractorContact,
   814  		MatrixName:                      user.MatrixName,
   815  		GitHubName:                      user.GitHubName,
   816  		SupervisorUserIDs:               superUserIDs,
   817  		ProposalsOwned:                  user.ProposalsOwned,
   818  	}
   819  }
   820  
   821  // issuanceDCCUser does the processing to move a nominated user to a fully
   822  // approved and invite them onto CMS.
   823  func (p *Politeiawww) issuanceDCCUser(userid, sponsorUserID string, domain, contractorType int) error {
   824  	nominatedUser, err := p.userByIDStr(userid)
   825  	if err != nil {
   826  		return err
   827  	}
   828  
   829  	if nominatedUser == nil {
   830  		return err
   831  	}
   832  
   833  	nomineeUserID, err := uuid.Parse(userid)
   834  	if err != nil {
   835  		return err
   836  	}
   837  	uu := user.UpdateCMSUser{
   838  		ID:             nomineeUserID,
   839  		ContractorType: contractorType,
   840  		Domain:         domain,
   841  	}
   842  
   843  	// If the nominee was an approved Subcontractor, then use the sponsor user
   844  	// ID as the SupervisorUserID
   845  	superVisorUserIDs := make([]uuid.UUID, 1)
   846  	if contractorType == int(cms.ContractorTypeSubContractor) {
   847  		parsed, err := uuid.Parse(sponsorUserID)
   848  		if err != nil {
   849  			return err
   850  		}
   851  		superVisorUserIDs[0] = parsed
   852  		uu.SupervisorUserIDs = superVisorUserIDs
   853  	}
   854  
   855  	payload, err := user.EncodeUpdateCMSUser(uu)
   856  	if err != nil {
   857  		return err
   858  	}
   859  	pc := user.PluginCommand{
   860  		ID:      user.CMSPluginID,
   861  		Command: user.CmdUpdateCMSUser,
   862  		Payload: string(payload),
   863  	}
   864  	_, err = p.db.PluginExec(pc)
   865  	if err != nil {
   866  		return err
   867  	}
   868  
   869  	// Try to email the verification link first; if it fails, then
   870  	// the new user won't be created.
   871  	//
   872  	// This is conditional on the email server being setup.
   873  	err = p.emailUserDCCApproved(nominatedUser.Email)
   874  	if err != nil {
   875  		log.Errorf("processApproveDCC: verification email "+
   876  			"failed for '%v': %v", nominatedUser.Email, err)
   877  		return err
   878  	}
   879  
   880  	return nil
   881  }
   882  
   883  func (p *Politeiawww) revokeDCCUser(userid string) error {
   884  	// Do full userdb update and reject user creds
   885  	nomineeUserID, err := uuid.Parse(userid)
   886  	if err != nil {
   887  		return err
   888  	}
   889  	uu := user.UpdateCMSUser{
   890  		ID:             nomineeUserID,
   891  		ContractorType: int(cms.ContractorTypeRevoked),
   892  	}
   893  	payload, err := user.EncodeUpdateCMSUser(uu)
   894  	if err != nil {
   895  		return err
   896  	}
   897  	pc := user.PluginCommand{
   898  		ID:      user.CMSPluginID,
   899  		Command: user.CmdUpdateCMSUser,
   900  		Payload: string(payload),
   901  	}
   902  	_, err = p.db.PluginExec(pc)
   903  	if err != nil {
   904  		return err
   905  	}
   906  	return nil
   907  }
   908  
   909  func (p *Politeiawww) processUserSubContractors(u *user.User) (*cms.UserSubContractorsReply, error) {
   910  	usc := user.CMSUserSubContractors{
   911  		ID: u.ID.String(),
   912  	}
   913  	payload, err := user.EncodeCMSUserSubContractors(usc)
   914  	if err != nil {
   915  		return nil, err
   916  	}
   917  	pc := user.PluginCommand{
   918  		ID:      user.CMSPluginID,
   919  		Command: user.CmdCMSUserSubContractors,
   920  		Payload: string(payload),
   921  	}
   922  	payloadReply, err := p.db.PluginExec(pc)
   923  	if err != nil {
   924  		return nil, err
   925  	}
   926  	cmsUsers, err := user.DecodeCMSUserSubContractorsReply([]byte(payloadReply.Payload))
   927  	if err != nil {
   928  		return nil, err
   929  	}
   930  	convertedCMSUsers := make([]cms.User, 0, len(cmsUsers.Users))
   931  	for _, uu := range cmsUsers.Users {
   932  		converted := convertCMSUserFromDatabaseUser(&uu)
   933  		convertedCMSUsers = append(convertedCMSUsers, converted)
   934  	}
   935  	uscr := &cms.UserSubContractorsReply{
   936  		Users: convertedCMSUsers,
   937  	}
   938  	return uscr, nil
   939  }
   940  
   941  // processProposalOwner returns a list of cms users given a proposal token.
   942  // If the user is set to have ownership of this proposal then they will be
   943  // added to the list.
   944  func (p *Politeiawww) processProposalOwner(po cms.ProposalOwner) (*cms.ProposalOwnerReply, error) {
   945  	log.Tracef("processProposalOwner")
   946  
   947  	// Setup plugin command
   948  	cupt := user.CMSUsersByProposalToken{
   949  		Token: po.ProposalToken,
   950  	}
   951  	payload, err := user.EncodeCMSUsersByProposalToken(cupt)
   952  	if err != nil {
   953  		return nil, err
   954  	}
   955  	pc := user.PluginCommand{
   956  		ID:      user.CMSPluginID,
   957  		Command: user.CmdCMSUsersByProposalToken,
   958  		Payload: string(payload),
   959  	}
   960  
   961  	// Execute plugin command
   962  	pcr, err := p.db.PluginExec(pc)
   963  	if err != nil {
   964  		return nil, err
   965  	}
   966  
   967  	// Decode reply
   968  	reply, err := user.DecodeCMSUsersByProposalTokenReply([]byte(pcr.Payload))
   969  	if err != nil {
   970  		return nil, err
   971  	}
   972  
   973  	matchedUsers := make([]cms.AbridgedCMSUser, 0, len(reply.Users))
   974  	for _, u := range reply.Users {
   975  		matchedUsers = append(matchedUsers, cms.AbridgedCMSUser{
   976  			ID:             u.ID.String(),
   977  			Username:       u.Username,
   978  			Domain:         cms.DomainTypeT(u.Domain),
   979  			ContractorType: cms.ContractorTypeT(u.ContractorType),
   980  		})
   981  	}
   982  	// Sort results alphabetically.
   983  	sort.Slice(matchedUsers, func(i, j int) bool {
   984  		return matchedUsers[i].Username < matchedUsers[j].Username
   985  	})
   986  
   987  	return &cms.ProposalOwnerReply{
   988  		Users: matchedUsers,
   989  	}, nil
   990  }