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 )