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

     1  // Copyright (c) 2022 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 main
     6  
     7  import (
     8  	"context"
     9  	"database/sql"
    10  	"fmt"
    11  	"regexp"
    12  
    13  	plugin "github.com/decred/politeia/politeiawww/plugin/v1"
    14  	"github.com/decred/politeia/politeiawww/user"
    15  	"github.com/google/uuid"
    16  	"github.com/pkg/errors"
    17  )
    18  
    19  // newUserCmd executes a plugin command that results in the creation of a new
    20  // user in the user database.
    21  //
    22  // Any updates made to the session will be persisted by the caller.
    23  //
    24  // This function assumes the caller has verified that the plugin command is
    25  // for the user plugin.
    26  func (p *politeiawww) newUserCmd(ctx context.Context, session *plugin.Session, cmd plugin.Cmd) (*plugin.Reply, error) {
    27  	log.Tracef("newUserCmd: %v %v %v", cmd.PluginID, cmd.Cmd, session.UserID)
    28  
    29  	// Setup the database transaction
    30  	tx, cancel, err := p.beginTx()
    31  	if err != nil {
    32  		return nil, err
    33  	}
    34  	defer cancel()
    35  
    36  	// Setup a new user
    37  	usr := &user.User{
    38  		ID:      uuid.New(),
    39  		Plugins: make(map[string]user.PluginData, 64),
    40  	}
    41  
    42  	// Verify that the session user, if one exists,
    43  	// is authorized to execute this plugin command.
    44  	reply, err := p.authorize(session, usr, cmd)
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  	if reply != nil {
    49  		return reply, nil
    50  	}
    51  
    52  	// Execute the pre plugin hooks
    53  	reply, err = p.preHooks(tx, usr,
    54  		plugin.HookArgs{
    55  			Type:  plugin.HookPreNewUser,
    56  			Cmd:   cmd,
    57  			Reply: nil,
    58  			User:  nil, // User is set in preHooks()
    59  		})
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  	if reply != nil {
    64  		return reply, nil
    65  	}
    66  
    67  	// Execute the new user plugin command
    68  	pluginUser := convertUser(usr, cmd.PluginID)
    69  	reply, err = p.userManager.NewUser(tx,
    70  		plugin.WriteArgs{
    71  			Cmd:  cmd,
    72  			User: pluginUser,
    73  		})
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  	if reply.Error != nil {
    78  		// The plugin command encountered
    79  		// a user error. Return it.
    80  		return reply, nil
    81  	}
    82  
    83  	// Update the global user object with any changes
    84  	// that the plugin made to the plugin user data.
    85  	updateUser(usr, pluginUser, cmd.PluginID)
    86  
    87  	// Execute the post plugin hooks
    88  	err = p.postHooks(tx, usr,
    89  		plugin.HookArgs{
    90  			Type:  plugin.HookPostNewUser,
    91  			Cmd:   cmd,
    92  			Reply: reply,
    93  			User:  nil, // User is set in postHooks()
    94  		})
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  
    99  	// Insert the user into the database
   100  	err = p.userDB.TxInsert(tx, *usr)
   101  	if err != nil {
   102  		return nil, err
   103  	}
   104  
   105  	// Commit the database transaction
   106  	err = tx.Commit()
   107  	if err != nil {
   108  		// Attempt to roll back the transaction
   109  		if err2 := tx.Rollback(); err2 != nil {
   110  			// We're in trouble!
   111  			panic(fmt.Sprintf("commit err: %v, rollback err: %v", err, err2))
   112  		}
   113  		return nil, err
   114  	}
   115  
   116  	return reply, nil
   117  }
   118  
   119  // writeCmd executes a plugin command that writes data.
   120  //
   121  // Any updates made to the session will be persisted by the caller.
   122  //
   123  // This function assumes the caller has verified that the plugin is a
   124  // registered plugin.
   125  func (p *politeiawww) writeCmd(ctx context.Context, session *plugin.Session, cmd plugin.Cmd) (*plugin.Reply, error) {
   126  	log.Tracef("writeCmd: %v %v %v", cmd.PluginID, cmd.Cmd, session.UserID)
   127  
   128  	// Setup the database transaction
   129  	tx, cancel, err := p.beginTx()
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  	defer cancel()
   134  
   135  	// Get the user. The session user ID should always
   136  	// exist for writes if the user layer is enabled.
   137  	// If the user layer is disabled, the user ID will
   138  	// be empty.
   139  	var usr *user.User
   140  	if session.UserID != "" {
   141  		usr, err = p.userDB.TxGet(tx, session.UserID)
   142  		if err != nil {
   143  			return nil, err
   144  		}
   145  	}
   146  
   147  	// Verify that the user is authorized to
   148  	// execute this plugin command.
   149  	reply, err := p.authorize(session, usr, cmd)
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  	if reply != nil {
   154  		return reply, nil
   155  	}
   156  
   157  	// Execute the pre plugin hooks
   158  	reply, err = p.preHooks(tx, usr,
   159  		plugin.HookArgs{
   160  			Type:  plugin.HookPreWrite,
   161  			Cmd:   cmd,
   162  			Reply: nil,
   163  			User:  nil, // User is set in preHooks()
   164  		})
   165  	if err != nil {
   166  		return nil, err
   167  	}
   168  	if reply != nil {
   169  		return reply, nil
   170  	}
   171  
   172  	// Execute the plugin command
   173  	plug, ok := p.plugins[cmd.PluginID]
   174  	if !ok {
   175  		// Should not happen
   176  		return nil, errors.Errorf("plugin not found: %v", cmd.PluginID)
   177  	}
   178  	pluginUser := convertUser(usr, cmd.PluginID)
   179  	reply, err = plug.WriteTx(tx,
   180  		plugin.WriteArgs{
   181  			Cmd:  cmd,
   182  			User: pluginUser,
   183  		})
   184  	if err != nil {
   185  		return nil, err
   186  	}
   187  	if reply.Error != nil {
   188  		// The plugin command encountered
   189  		// a user error. Return it.
   190  		return reply, nil
   191  	}
   192  
   193  	// Update the global user object with any changes
   194  	// that the plugin made to the plugin user data.
   195  	updateUser(usr, pluginUser, cmd.PluginID)
   196  
   197  	// Execute the post plugin hooks
   198  	err = p.postHooks(tx, usr,
   199  		plugin.HookArgs{
   200  			Type:  plugin.HookPostWrite,
   201  			Cmd:   cmd,
   202  			Reply: reply,
   203  			User:  nil, // User is set in postHooks()
   204  		})
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  
   209  	// Update the user in the database if any
   210  	// updates were made to the user data.
   211  	if usr != nil && usr.Updated {
   212  		err = p.userDB.TxUpdate(tx, *usr)
   213  		if err != nil {
   214  			return nil, err
   215  		}
   216  	}
   217  
   218  	// Commit the database transaction
   219  	err = tx.Commit()
   220  	if err != nil {
   221  		// Attempt to roll back the transaction
   222  		if err2 := tx.Rollback(); err2 != nil {
   223  			// We're in trouble!
   224  			panic(fmt.Sprintf("commit err: %v, rollback err: %v", err, err2))
   225  		}
   226  		return nil, err
   227  	}
   228  
   229  	return reply, nil
   230  }
   231  
   232  // readCmd executes a read-only plugin command. The read operation is not
   233  // atomic.
   234  //
   235  // Any updates made to the session will be persisted by the caller.
   236  //
   237  // This function assumes the caller has verified that the plugin is a
   238  // registered plugin.
   239  func (p *politeiawww) readCmd(ctx context.Context, session *plugin.Session, cmd plugin.Cmd) (*plugin.Reply, error) {
   240  	log.Tracef("readCmd: %v %v %v", cmd.PluginID, cmd.Cmd, session.UserID)
   241  
   242  	// Get the user. A session user may or may not exist.
   243  	var (
   244  		usr *user.User
   245  		err error
   246  	)
   247  	if session.UserID != "" {
   248  		usr, err = p.userDB.Get(session.UserID)
   249  		if err != nil {
   250  			return nil, err
   251  		}
   252  	}
   253  
   254  	// Verify that the user is authorized to
   255  	// execute this plugin command.
   256  	reply, err := p.authorize(session, usr, cmd)
   257  	if err != nil {
   258  		return nil, err
   259  	}
   260  	if reply != nil {
   261  		return reply, nil
   262  	}
   263  
   264  	// Execute the plugin command
   265  	plug, ok := p.plugins[cmd.PluginID]
   266  	if !ok {
   267  		// Should not happen
   268  		return nil, errors.Errorf("plugin not found: %v", cmd.PluginID)
   269  	}
   270  	reply, err = plug.Read(plugin.ReadArgs{
   271  		Cmd:  cmd,
   272  		User: convertUser(usr, cmd.PluginID),
   273  	})
   274  	if err != nil {
   275  		return nil, err
   276  	}
   277  	if reply.Error != nil {
   278  		// The plugin command encountered
   279  		// a user error. Return it.
   280  		return reply, nil
   281  	}
   282  
   283  	return reply, nil
   284  }
   285  
   286  // preHooks executes the provided pre hook for all plugins. Pre hooks are used
   287  // to perform validation on the plugin command.
   288  //
   289  // A plugin reply will be returned if one of the plugins throws a user error
   290  // during hook execution. The user error will be embedded in the plugin
   291  // reply. Unexpected errors result in a standard golang error being returned.
   292  func (p *politeiawww) preHooks(tx *sql.Tx, usr *user.User, h plugin.HookArgs) (*plugin.Reply, error) {
   293  	err := p.hook(tx, h, usr)
   294  	if err != nil {
   295  		var ue plugin.UserError
   296  		if errors.As(err, &ue) {
   297  			return &plugin.Reply{
   298  				Error: err,
   299  			}, nil
   300  		}
   301  		return nil, err
   302  	}
   303  	return nil, nil
   304  }
   305  
   306  // postHooks executes the provided post hook for all user plugins.
   307  //
   308  // Post hooks are not able to throw plugin errors like the pre hooks are. Any
   309  // error returned by a plugin from a post hook will be treated as an unexpected
   310  // error.
   311  func (p *politeiawww) postHooks(tx *sql.Tx, usr *user.User, h plugin.HookArgs) error {
   312  	return p.hook(tx, h, usr)
   313  }
   314  
   315  // hook executes a hook on on all plugins. A sql Tx may or may not exist
   316  // depending on the whether the caller is executing an atomic operation.
   317  func (p *politeiawww) hook(tx *sql.Tx, h plugin.HookArgs, usr *user.User) error {
   318  	for _, pluginID := range p.pluginIDs {
   319  		// Get the plugin
   320  		p, ok := p.plugins[pluginID]
   321  		if !ok {
   322  			// Should not happen
   323  			return errors.Errorf("plugin not found: %v", pluginID)
   324  		}
   325  
   326  		// Add the plugin user to the hook payload
   327  		h.User = convertUser(usr, h.Cmd.PluginID)
   328  
   329  		// Execute the hook. Some commands will execute
   330  		// the hook using a database transaction (write
   331  		// commands) and some won't (read-only commands).
   332  		if tx != nil {
   333  			err := p.HookTx(tx, h)
   334  			if err != nil {
   335  				return err
   336  			}
   337  		} else {
   338  			err := p.Hook(h)
   339  			if err != nil {
   340  				return err
   341  			}
   342  		}
   343  
   344  		// Update the global user object with any changes
   345  		// that the plugin made to the plugin user data.
   346  		updateUser(usr, h.User, h.Cmd.PluginID)
   347  	}
   348  
   349  	return nil
   350  }
   351  
   352  func (p *politeiawww) authorize(s *plugin.Session, usr *user.User, cmd plugin.Cmd) (*plugin.Reply, error) {
   353  	// Setup the plugin user
   354  	pluginUser := convertUser(usr, p.authManager.ID())
   355  
   356  	// Check user authorization
   357  	err := p.authManager.Authorize(
   358  		plugin.AuthorizeArgs{
   359  			Session:  s,
   360  			User:     pluginUser,
   361  			PluginID: cmd.PluginID,
   362  			Version:  cmd.Version,
   363  			Cmd:      cmd.Cmd,
   364  		})
   365  	if err != nil {
   366  		var ue plugin.UserError
   367  		if errors.As(err, &ue) {
   368  			return &plugin.Reply{
   369  				Error: err,
   370  			}, nil
   371  		}
   372  		return nil, err
   373  	}
   374  
   375  	// Update the global user object with any changes
   376  	// that the plugin made to the plugin user data.
   377  	updateUser(usr, pluginUser, cmd.PluginID)
   378  
   379  	return nil, nil
   380  }
   381  
   382  // updateUser updates the global user object with any changes that were made
   383  // to the plugin user object during plugin command execution.
   384  func updateUser(u *user.User, p *plugin.User, pluginID string) {
   385  	if !p.PluginData.Updated() {
   386  		return
   387  	}
   388  
   389  	pluginData := u.Plugins[pluginID]
   390  	pluginData.ClearText = p.PluginData.ClearText()
   391  	pluginData.Encrypted = p.PluginData.Encrypted()
   392  
   393  	u.Plugins[pluginID] = pluginData
   394  	u.Updated = true
   395  }
   396  
   397  // convertUser converts a global user to a plugin user. Only the plugin data
   398  // for the provided plugin ID is included in the plugin user object. This
   399  // prevents plugins from accessing data that they do not own.
   400  func convertUser(u *user.User, pluginID string) *plugin.User {
   401  	pluginData := u.Plugins[pluginID]
   402  	return &plugin.User{
   403  		ID: u.ID,
   404  		PluginData: plugin.NewPluginData(pluginData.ClearText,
   405  			pluginData.Encrypted),
   406  	}
   407  }
   408  
   409  var (
   410  	// regexpPluginSettingMulti matches against the plugin setting
   411  	// value when it contains multiple values.
   412  	//
   413  	// pluginID,key,["value1","value2"] matches ["value1","value2"]
   414  	regexpPluginSettingMulti = regexp.MustCompile(`(\[.*\]$)`)
   415  )