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

     1  // Copyright (c) 2017 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 user
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/hex"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"time"
    14  
    15  	"github.com/decred/politeia/politeiad/api/v1/identity"
    16  	"github.com/google/uuid"
    17  )
    18  
    19  var (
    20  	// ErrSessionNotFound indicates that a user session was not found
    21  	// in the database.
    22  	ErrSessionNotFound = errors.New("no user session found")
    23  
    24  	// ErrUserNotFound indicates that a user name was not found in the
    25  	// database.
    26  	ErrUserNotFound = errors.New("user not found")
    27  
    28  	// ErrUserExists indicates that a user already exists in the
    29  	// database.
    30  	ErrUserExists = errors.New("user already exists")
    31  
    32  	// ErrShutdown is emitted when the database is shutting down.
    33  	ErrShutdown = errors.New("database is shutting down")
    34  
    35  	// ErrInvalidPlugin is emitted when a invalid plugin is used.
    36  	ErrInvalidPlugin = errors.New("invalid plugin")
    37  
    38  	// ErrInvalidPluginCmd is emitted when an invalid plugin command
    39  	// is used.
    40  	ErrInvalidPluginCmd = errors.New("invalid plugin command")
    41  
    42  	// ErrCodeStatsNotFound indicates that an requested code stats entry wasn't
    43  	// found.
    44  	ErrCodeStatsNotFound = errors.New("code stats not found")
    45  )
    46  
    47  // Identity wraps an ed25519 public key and timestamps to indicate if it is
    48  // active. An identity can be in one of three states: inactive, active, or
    49  // deactivated.
    50  //
    51  // inactive: Activated == 0 && Deactivated == 0
    52  // The identity has been created, but has not yet been activated.
    53  //
    54  // active: Activated != 0 && Deactivated == 0
    55  // The identity has been created and has been activated.
    56  //
    57  // deactivated: Deactivated != 0
    58  // The identity in no longer active and the key is no longer valid. Both
    59  // inactive and active identities can be marked as deactivated. An inactive
    60  // identity being deactivated means that the identity was never verified before
    61  // a newer identity was created. An active identity being deactivated means
    62  // that a newer identity was created to replace the active identity.
    63  type Identity struct {
    64  	Key         [identity.PublicKeySize]byte `json:"key"`         // ed25519 public key
    65  	Activated   int64                        `json:"activated"`   // Time key as activated for use
    66  	Deactivated int64                        `json:"deactivated"` // Time key was deactivated
    67  }
    68  
    69  // Activate activates the identity by setting the activated timestamp.
    70  func (i *Identity) Activate() {
    71  	i.Activated = time.Now().Unix()
    72  }
    73  
    74  // Deactivate deactivates the identity by setting the deactivated timestamp.
    75  func (i *Identity) Deactivate() {
    76  	i.Deactivated = time.Now().Unix()
    77  }
    78  
    79  // IsInactive returns whether the identity is inactive. See the Identity
    80  // definition for more info on inactive identities.
    81  func (i *Identity) IsInactive() bool {
    82  	return i.Activated == 0 && i.Deactivated == 0
    83  }
    84  
    85  // IsActive returns whether the identity is active. See the Identity definition
    86  // for more info on active identities.
    87  func (i *Identity) IsActive() bool {
    88  	return i.Activated != 0 && i.Deactivated == 0
    89  }
    90  
    91  // IsDeactivated returns whether the identity has been deactivated. See the
    92  // Identity definition for more info on deactivated identities.
    93  func (i *Identity) IsDeactivated() bool {
    94  	return i.Deactivated != 0
    95  }
    96  
    97  // Status returns whether the identity is inactive, active, or deactivated.
    98  func (i *Identity) Status() string {
    99  	switch {
   100  	case i.IsInactive():
   101  		return "inactive"
   102  	case i.IsActive():
   103  		return "active"
   104  	case i.IsDeactivated():
   105  		return "deactivated"
   106  	}
   107  	return "invalid"
   108  }
   109  
   110  // String returns a hex encoded string of the identity key.
   111  func (i *Identity) String() string {
   112  	return hex.EncodeToString(i.Key[:])
   113  }
   114  
   115  // NewIdentity returns a new inactive identity that was created using the
   116  // provided public key.
   117  func NewIdentity(publicKey string) (*Identity, error) {
   118  	b, err := hex.DecodeString(publicKey)
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  
   123  	var empty [identity.PublicKeySize]byte
   124  	switch {
   125  	case len(b) != len(empty):
   126  		return nil, fmt.Errorf("invalid length")
   127  	case bytes.Equal(b, empty[:]):
   128  		return nil, fmt.Errorf("empty bytes")
   129  	}
   130  
   131  	id := Identity{}
   132  	copy(id.Key[:], b)
   133  	return &id, nil
   134  }
   135  
   136  // A proposal paywall allows the user to purchase proposal credits.  Proposal
   137  // paywalls are only valid for one tx.  The number of proposal credits created
   138  // is determined by dividing the tx amount by the credit price.  Proposal
   139  // paywalls expire after a set duration. politeiawww polls the paywall address
   140  // for a payment tx until the paywall is either paid or it expires.
   141  type ProposalPaywall struct {
   142  	ID          uint64 `json:"id"`          // Paywall ID
   143  	CreditPrice uint64 `json:"creditprice"` // Cost per proposal credit in atoms
   144  	Address     string `json:"address"`     // Paywall address
   145  	TxNotBefore int64  `json:"txnotbefore"` // Unix timestamp of minimum timestamp for paywall tx
   146  	PollExpiry  int64  `json:"pollexpiry"`  // Unix timestamp of expiration time of paywall polling
   147  	TxID        string `json:"txid"`        // Payment transaction ID
   148  	TxAmount    uint64 `json:"txamount"`    // Amount sent to paywall address in atoms
   149  	NumCredits  uint64 `json:"numcredits"`  // Number of proposal credits created by payment tx
   150  }
   151  
   152  // A proposal credit allows the user to submit a new proposal.  Credits are
   153  // created when a user sends a payment to a proposal paywall.  A credit is
   154  // automatically spent when a user submits a new proposal.  When a credit is
   155  // spent, it is updated with the proposal's censorship token and moved to the
   156  // user's spent proposal credits list.
   157  type ProposalCredit struct {
   158  	PaywallID       uint64 `json:"paywallid"`       // Proposal paywall ID of associated paywall
   159  	Price           uint64 `json:"price"`           // Credit price in atoms
   160  	DatePurchased   int64  `json:"datepurchased"`   // Unix timestamp of credit purchase
   161  	TxID            string `json:"txid"`            // Payment transaction ID
   162  	CensorshipToken string `json:"censorshiptoken"` // Token of proposal that spent this credit
   163  }
   164  
   165  // VersionUser is the version of the User struct.
   166  const VersionUser uint32 = 1
   167  
   168  // User represents a politeiawww user.
   169  type User struct {
   170  	ID                  uuid.UUID `json:"id"`                  // Unique user uuid
   171  	Email               string    `json:"email"`               // Email address
   172  	Username            string    `json:"username"`            // Unique username
   173  	HashedPassword      []byte    `json:"hashedpassword"`      // Blowfish hash
   174  	Admin               bool      `json:"admin"`               // Is user an admin
   175  	EmailNotifications  uint64    `json:"emailnotifications"`  // Email notification setting
   176  	LastLoginTime       int64     `json:"lastlogintime"`       // Unix timestamp of last login
   177  	FailedLoginAttempts uint64    `json:"failedloginattempts"` // Sequential failed login attempts
   178  	Deactivated         bool      `json:"deactivated"`         // Is account deactivated
   179  
   180  	// Verification tokens and their expirations
   181  	NewUserVerificationToken        []byte `json:"newuserverificationtoken"`
   182  	NewUserVerificationExpiry       int64  `json:"newuserverificationtokenexiry"`
   183  	ResendNewUserVerificationExpiry int64  `json:"resendnewuserverificationtoken"`
   184  	UpdateKeyVerificationToken      []byte `json:"updatekeyverificationtoken"`
   185  	UpdateKeyVerificationExpiry     int64  `json:"updatekeyverificationexpiry"`
   186  	ResetPasswordVerificationToken  []byte `json:"resetpasswordverificationtoken"`
   187  	ResetPasswordVerificationExpiry int64  `json:"resetpasswordverificationexpiry"`
   188  
   189  	// PaywallAddressIndex is the index that is used to generate the
   190  	// paywall address for the user. The same paywall address is used
   191  	// for the user registration paywall and for proposal credit
   192  	// paywalls. The index is set during the new user record creation
   193  	// and is sequential.
   194  	// XXX why is this an uint64 when hdkeychain requires a uint32?
   195  	PaywallAddressIndex uint64 `json:"paywalladdressindex"`
   196  
   197  	// User registration paywall info
   198  	NewUserPaywallAddress string `json:"newuserpaywalladdress"`
   199  	NewUserPaywallAmount  uint64 `json:"newuserpaywallamount"`
   200  	NewUserPaywallTx      string `json:"newuserpaywalltx"`
   201  
   202  	// NewUserPaywallTxNotBeore is the minimum UNIX time (in seconds)
   203  	// required for the block containing the transaction sent to
   204  	// NewUserPaywallAddress. If the user has already paid, this field
   205  	// will be empty.
   206  	NewUserPaywallTxNotBefore int64 `json:"newuserpaywalltxnotbefore"`
   207  
   208  	// The UNIX time (in seconds) for when the server will stop polling
   209  	// the server for transactions at NewUserPaywallAddress. If the
   210  	// user has already paid, this field will be empty.
   211  	NewUserPaywallPollExpiry int64 `json:"newuserpaywallpollexpiry"`
   212  
   213  	// User access times for proposal comments. The access time is a
   214  	// Unix timestamp of the last time the user accessed a proposal's
   215  	// comments.
   216  	// [token]accessTime
   217  	ProposalCommentsAccessTimes map[string]int64 `json:"proposalcommentsaccesstime"`
   218  
   219  	// All identities the user has ever used. We allow the user to change
   220  	// identities to deal with key loss. An identity can be in one of three
   221  	// states: inactive, active, or deactivated.
   222  	//
   223  	// Inactive identities
   224  	// An identity is consider inactive until it has been verified.
   225  	// An unverified user will have an inactive identity.
   226  	// A user will only ever have one inactive identity at a time.
   227  	//
   228  	// Active identities
   229  	// A verified user will always have one active identity.
   230  	// A verified user may have both an active and inactive identity if
   231  	// they have requested a new identity but have not yet verified it.
   232  	//
   233  	// Deactivated identities
   234  	// An identity is deactivated when it is replaced by a new identity.
   235  	// The key of a deactivated identity is no longer valid.
   236  	// An identity cannot be re-activated once it has been deactivated.
   237  	Identities []Identity `json:"identities"`
   238  
   239  	// All proposal paywalls that have been issued to the user in chronological
   240  	// order.
   241  	ProposalPaywalls []ProposalPaywall `json:"proposalpaywalls"`
   242  
   243  	// All proposal credits that have been purchased by the user, but have not
   244  	// yet been used to submit a proposal.  Once a credit is used to submit a
   245  	// proposal, it is updated with the proposal's censorship token and moved to
   246  	// the user's spent proposal credits list.  The price that the proposal
   247  	// credit was purchased at is in atoms.
   248  	UnspentProposalCredits []ProposalCredit `json:"unspentproposalcredits"`
   249  
   250  	// All credits that have been purchased by the user and have already been
   251  	// used to submit proposals.  Spent credits have a proposal censorship token
   252  	// associated with them to signify that they have been spent. The price that
   253  	// the proposal credit was purchased at is in atoms.
   254  	SpentProposalCredits []ProposalCredit `json:"spentproposalcredits"`
   255  
   256  	// TOTP Secret Key and type of TOTP being used.
   257  	TOTPSecret             string  `json:"totpsecret"`
   258  	TOTPType               int     `json:"totptype"`
   259  	TOTPVerified           bool    `json:"totpverified"` // whether current totp secret has been verified with
   260  	TOTPLastUpdated        []int64 `json:"totplastupdated"`
   261  	TOTPLastFailedCodeTime []int64 `json:"totplastfailedcodetime"`
   262  }
   263  
   264  // ActiveIdentity returns the active identity for the user if one exists.
   265  func (u *User) ActiveIdentity() *Identity {
   266  	for k, v := range u.Identities {
   267  		if v.IsActive() {
   268  			return &u.Identities[k]
   269  		}
   270  	}
   271  	return nil
   272  }
   273  
   274  // InactiveIdentity returns the inactive identity for the user if one exists.
   275  func (u *User) InactiveIdentity() *Identity {
   276  	for k, v := range u.Identities {
   277  		if v.IsInactive() {
   278  			return &u.Identities[k]
   279  		}
   280  	}
   281  	return nil
   282  }
   283  
   284  // PublicKey returns a hex encoded string of the user's active identity.
   285  func (u *User) PublicKey() string {
   286  	if u.ActiveIdentity() != nil {
   287  		return u.ActiveIdentity().String()
   288  	}
   289  	return ""
   290  }
   291  
   292  // AddIdentity adds the provided inactive identity to the identities array for
   293  // the user. Any existing inactive identities are deactivated. A user should
   294  // only ever have one inactive identity at a time, but due to a prior bug, this
   295  // may not always be the case.
   296  func (u *User) AddIdentity(id Identity) error {
   297  	if u.Identities == nil {
   298  		u.Identities = make([]Identity, 0)
   299  	}
   300  
   301  	// Validate provided identity
   302  	for _, v := range u.Identities {
   303  		if bytes.Equal(v.Key[:], id.Key[:]) {
   304  			if v.IsInactive() {
   305  				// Inactive identity has already been
   306  				// added. This is ok.
   307  				return nil
   308  			}
   309  			return fmt.Errorf("duplicate key")
   310  		}
   311  	}
   312  	switch {
   313  	case id.Deactivated != 0:
   314  		return fmt.Errorf("identity is deactivated")
   315  	case id.Activated != 0:
   316  		return fmt.Errorf("identity is activated")
   317  	}
   318  
   319  	// Deactivate any existing inactive identities
   320  	for k, v := range u.Identities {
   321  		if v.IsInactive() {
   322  			u.Identities[k].Deactivate()
   323  		}
   324  	}
   325  
   326  	// Add new inactive identity
   327  	u.Identities = append(u.Identities, id)
   328  
   329  	return nil
   330  }
   331  
   332  // ActivateIdentity sets the identity associated with the provided key as the
   333  // active identity for the user. The provided key must correspond to an
   334  // inactive identity. If there is an existing active identity, it wil be
   335  // deactivated.
   336  func (u *User) ActivateIdentity(key []byte) error {
   337  	if u.Identities == nil {
   338  		return fmt.Errorf("identity not found")
   339  	}
   340  
   341  	// Ensure provided key exists and is inactive
   342  	var inactive *Identity
   343  	for k, v := range u.Identities {
   344  		if bytes.Equal(v.Key[:], key[:]) {
   345  			inactive = &u.Identities[k]
   346  			break
   347  		}
   348  	}
   349  	switch {
   350  	case inactive == nil:
   351  		return fmt.Errorf("identity not found")
   352  	case inactive.Deactivated != 0:
   353  		return fmt.Errorf("identity is deactivated")
   354  	case inactive.Activated != 0:
   355  		return fmt.Errorf("identity is activated")
   356  	}
   357  
   358  	// Deactivate any active identities. There should only ever be a
   359  	// single active identity at a time, but due to a prior bug in the
   360  	// early version of politeia, this may not hold true. Check all
   361  	// identities just to be sure.
   362  	for k, v := range u.Identities {
   363  		// Skip the inactive identity that is going
   364  		// to be the new active identity.
   365  		if inactive.String() == v.String() {
   366  			continue
   367  		}
   368  
   369  		if v.Deactivated == 0 {
   370  			u.Identities[k].Deactivate()
   371  		}
   372  	}
   373  
   374  	// Update the inactive identity to be active.
   375  	inactive.Activate()
   376  
   377  	return nil
   378  }
   379  
   380  // NotificationIsEnabled returns whether the user has the provided notification
   381  // bit enabled. This function will always return false if the user has been
   382  // deactivated.
   383  func (u *User) NotificationIsEnabled(ntfnBit uint64) bool {
   384  	if u.Deactivated {
   385  		return false
   386  	}
   387  	return u.EmailNotifications&ntfnBit != 0
   388  }
   389  
   390  // EncodeUser encodes User into a JSON byte slice.
   391  func EncodeUser(u User) ([]byte, error) {
   392  	b, err := json.Marshal(u)
   393  	if err != nil {
   394  		return nil, err
   395  	}
   396  
   397  	return b, nil
   398  }
   399  
   400  // DecodeUser decodes a JSON byte slice into a User.
   401  func DecodeUser(payload []byte) (*User, error) {
   402  	var u User
   403  
   404  	err := json.Unmarshal(payload, &u)
   405  	if err != nil {
   406  		return nil, err
   407  	}
   408  
   409  	return &u, nil
   410  }
   411  
   412  // PluginCommand is used to execute a plugin command.
   413  type PluginCommand struct {
   414  	ID      string // Plugin identifier
   415  	Command string // Command identifier
   416  	Payload string // Command payload
   417  }
   418  
   419  // PluginCommandReply is used to reply to a PluginCommand.
   420  type PluginCommandReply struct {
   421  	ID      string // Plugin identifier
   422  	Command string // Command identifier
   423  	Payload string // Command reply payload
   424  }
   425  
   426  // PluginSetting holds the key/value pair of a plugin setting.
   427  type PluginSetting struct {
   428  	Key   string // Name of setting
   429  	Value string // Value of setting
   430  }
   431  
   432  // Plugin describes a plugin and its settings.
   433  type Plugin struct {
   434  	ID       string
   435  	Version  string
   436  	Settings []PluginSetting
   437  }
   438  
   439  // Session represents a user session.
   440  //
   441  // ID is the decoded session ID. The ID present in the session cookie is the
   442  // encoded session ID. The encoding/decoding is handled by the session Store.
   443  //
   444  // Values are politeiawww specific encoded session values. The encoding is
   445  // handled by the session Store.
   446  //
   447  // UserID and CreatedAt are included in the encoded Values but have also been
   448  // broken out into their own fields so that they can be queryable. UserID
   449  // allows for lookups by UserID and CreatedAt allows for periodically cleaning
   450  // up expired sessions in the database.
   451  type Session struct {
   452  	ID        string    `json:"id"`        // Unique session ID
   453  	UserID    uuid.UUID `json:"userid"`    // User UUID
   454  	CreatedAt int64     `json:"createdat"` // Created at UNIX timestamp
   455  	Values    string    `json:"values"`    // Encoded session values
   456  }
   457  
   458  // VersionSession is the version of the Session struct.
   459  const VersionSession uint32 = 1
   460  
   461  // EncodeSession encodes Session into a JSON byte slice.
   462  func EncodeSession(s Session) ([]byte, error) {
   463  	b, err := json.Marshal(s)
   464  	if err != nil {
   465  		return nil, err
   466  	}
   467  
   468  	return b, nil
   469  }
   470  
   471  // DecodeSession decodes a JSON byte slice into a Session.
   472  func DecodeSession(payload []byte) (*Session, error) {
   473  	var s Session
   474  
   475  	err := json.Unmarshal(payload, &s)
   476  	if err != nil {
   477  		return nil, err
   478  	}
   479  
   480  	return &s, nil
   481  }
   482  
   483  // Database describes the interface used for interacting with the user
   484  // database.
   485  type Database interface {
   486  	// Add a new user
   487  	UserNew(User) error
   488  
   489  	// Update an existing user
   490  	UserUpdate(User) error
   491  
   492  	// Return user record given the username
   493  	UserGetByUsername(string) (*User, error)
   494  
   495  	// Return user record given its id
   496  	UserGetById(uuid.UUID) (*User, error)
   497  
   498  	// Return user record given a public key
   499  	UserGetByPubKey(string) (*User, error)
   500  
   501  	// Return a map of public key to user record
   502  	UsersGetByPubKey(pubKeys []string) (map[string]User, error)
   503  
   504  	// Insert a user to the database.
   505  	// Intended to be used for migration between databases.
   506  	InsertUser(User) error
   507  
   508  	// Iterate over all users
   509  	AllUsers(callbackFn func(u *User)) error
   510  
   511  	// Create or update a user session
   512  	SessionSave(Session) error
   513  
   514  	// Return a user session given its id
   515  	SessionGetByID(sessionID string) (*Session, error)
   516  
   517  	// Delete a user session given its id
   518  	SessionDeleteByID(sessionID string) error
   519  
   520  	// Delete all sessions for a user except for the given session IDs
   521  	SessionsDeleteByUserID(id uuid.UUID, exemptSessionIDs []string) error
   522  
   523  	// SetPaywallAddressIndex updates the paywall address index.
   524  	SetPaywallAddressIndex(index uint64) error
   525  
   526  	// Rotate encryption keys
   527  	RotateKeys(newKeyPath string) error
   528  
   529  	// Register a plugin
   530  	RegisterPlugin(Plugin) error
   531  
   532  	// Execute a plugin command
   533  	PluginExec(PluginCommand) (*PluginCommandReply, error)
   534  
   535  	// Close performs cleanup of the backend.
   536  	Close() error
   537  }