github.com/decred/politeia@v1.4.0/politeiawww/cmd/cmswww/cmswww.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 main
     6  
     7  import (
     8  	"bufio"
     9  	"encoding/csv"
    10  	"encoding/hex"
    11  	"errors"
    12  	"fmt"
    13  	"net/url"
    14  	"os"
    15  	"strconv"
    16  	"strings"
    17  
    18  	"github.com/decred/dcrd/dcrutil/v3"
    19  	"github.com/decred/politeia/politeiad/api/v1/identity"
    20  	cms "github.com/decred/politeia/politeiawww/api/cms/v1"
    21  	pi "github.com/decred/politeia/politeiawww/api/www/v1"
    22  	"github.com/decred/politeia/politeiawww/cmd/shared"
    23  	"github.com/decred/politeia/util"
    24  	flags "github.com/jessevdk/go-flags"
    25  )
    26  
    27  const (
    28  	// Config settings
    29  	defaultHomeDirname    = "cmswww"
    30  	defaultDataDirname    = "data"
    31  	defaultConfigFilename = "cmswww.conf"
    32  )
    33  
    34  var (
    35  	// Global variables for cmswww commands
    36  	cfg    *shared.Config
    37  	client *shared.Client
    38  
    39  	// Config settings
    40  	defaultHomeDir = dcrutil.AppDataDir(defaultHomeDirname, false)
    41  
    42  	// errInvoiceCSVNotFound is emitted when a invoice csv file is
    43  	// required but has not been passed into the command.
    44  	errInvoiceCSVNotFound = errors.New("invoice csv file not found. " +
    45  		"You must either provide a csv file or use the --random flag.")
    46  
    47  	// errInvalidDCCType is emitted if there is a bad dcc type used.
    48  	errInvalidDCCType = errors.New("submitted dcc type is invalid," +
    49  		"must be 1 (Issuance) or 2 (Revocation)")
    50  )
    51  
    52  type cmswww struct {
    53  	// XXX the config does not need to be a part of this struct, but
    54  	// is included so that the config cli flags print as part of the
    55  	// cmswww help message. This is handled by go-flags.
    56  	Config shared.Config
    57  
    58  	// Commands
    59  	ActiveVotes            ActiveVotesCmd               `command:"activevotes" description:"(user) get the dccs that are being voted on"`
    60  	BatchProposals         BatchProposalsCmd            `command:"batchproposals" description:"(user)   retrieve a set of proposals"`
    61  	NewComment             NewCommentCmd                `command:"newcomment" description:"(user)   create a new comment"`
    62  	CensorComment          CensorCommentCmd             `command:"censorcomment" description:"(admin)  censor a comment"`
    63  	ChangePassword         shared.UserPasswordChangeCmd `command:"changepassword" description:"(user)   change the password for the logged in user"`
    64  	ChangeUsername         shared.UserUsernameChangeCmd `command:"changeusername" description:"(user)   change the username for the logged in user"`
    65  	CMSUsers               CMSUsersCmd                  `command:"cmsusers" description:"(user)   get a list of cms users"`
    66  	CodeStats              CodeStatsCmd                 `command:"codestats" description:"(user)    get a list of code stats per repo for the given userid"`
    67  	DCCComments            DCCCommentsCmd               `command:"dcccomments" description:"(user)   get the comments for a dcc proposal"`
    68  	DCCDetails             DCCDetailsCmd                `command:"dccdetails" description:"(user)   get the details of a dcc"`
    69  	EditInvoice            EditInvoiceCmd               `command:"editinvoice" description:"(user)   edit a invoice"`
    70  	EditUser               EditUserCmd                  `command:"edituser" description:"(user)   edit current cms user information"`
    71  	GeneratePayouts        GeneratePayoutsCmd           `command:"generatepayouts" description:"(admin)  generate a list of payouts with addresses and amounts to pay"`
    72  	GetDCCs                GetDCCsCmd                   `command:"getdccs" description:"(user)   get all dccs (optional by status)"`
    73  	Help                   HelpCmd                      `command:"help" description:"         print a detailed help message for a specific command"`
    74  	InvoiceComments        InvoiceCommentsCmd           `command:"invoicecomments" description:"(user)   get the comments for a invoice"`
    75  	InvoiceExchangeRate    InvoiceExchangeRateCmd       `command:"invoiceexchangerate" description:"(user)   get exchange rate for a given month/year"`
    76  	InviteNewUser          InviteNewUserCmd             `command:"invite" description:"(admin)  invite a new user"`
    77  	InvoiceDetails         InvoiceDetailsCmd            `command:"invoicedetails" description:"(public) get the details of a proposal"`
    78  	InvoicePayouts         InvoicePayoutsCmd            `command:"invoicepayouts" description:"(admin)  generate paid invoice list for a given date range"`
    79  	Invoices               InvoicesCmd                  `command:"invoices" description:"(user)  get all invoices (optional with optional parameters)"`
    80  	Login                  shared.LoginCmd              `command:"login" description:"(public) login to Politeia"`
    81  	Logout                 shared.LogoutCmd             `command:"logout" description:"(public) logout of Politeia"`
    82  	CMSManageUser          CMSManageUserCmd             `command:"cmsmanageuser" description:"(admin)  edit certain properties of the specified user"`
    83  	ManageUser             shared.UserManageCmd         `command:"manageuser" description:"(admin)  edit certain properties of the specified user"`
    84  	Me                     shared.MeCmd                 `command:"me" description:"(user)   get user details for the logged in user"`
    85  	NewDCC                 NewDCCCmd                    `command:"newdcc" description:"(user)   creates a new dcc proposal"`
    86  	NewDCCComment          NewDCCCommentCmd             `command:"newdcccomment" description:"(user)   creates a new comment on a dcc proposal"`
    87  	NewInvoice             NewInvoiceCmd                `command:"newinvoice" description:"(user)   create a new invoice"`
    88  	PayInvoices            PayInvoicesCmd               `command:"payinvoices" description:"(admin)  set all approved invoices to paid"`
    89  	Policy                 PolicyCmd                    `command:"policy" description:"(public) get the server policy"`
    90  	ProposalOwner          ProposalOwnerCmd             `command:"proposalowner" description:"(user) get owners of a proposal"`
    91  	ProposalBilling        ProposalBillingCmd           `command:"proposalbilling" description:"(user) get billing information for a proposal"`
    92  	ProposalBillingDetails ProposalBillingDetailsCmd    `command:"proposalbillingdetails" description:"(admin) get billing information for a proposal"`
    93  	ProposalBillingSummary ProposalBillingSummaryCmd    `command:"proposalbillingsummary" description:"(admin) get all approved proposal billing information"`
    94  	RegisterUser           RegisterUserCmd              `command:"register" description:"(public) register an invited user to cms"`
    95  	ResetPassword          shared.UserPasswordResetCmd  `command:"resetpassword" description:"(public) reset the password for a user that is not logged in"`
    96  	SetDCCStatus           SetDCCStatusCmd              `command:"setdccstatus" description:"(admin)  set the status of a DCC"`
    97  	SetInvoiceStatus       SetInvoiceStatusCmd          `command:"setinvoicestatus" description:"(admin)  set the status of an invoice"`
    98  	SetTOTP                shared.UserTOTPSetCmd        `command:"settotp" description:"(user)  set the key for TOTP"`
    99  	StartVote              StartVoteCmd                 `command:"startvote" description:"(admin)  start the voting period on a dcc"`
   100  	SupportOpposeDCC       SupportOpposeDCCCmd          `command:"supportopposedcc" description:"(user)   support or oppose a given DCC"`
   101  	TestRun                TestRunCmd                   `command:"testrun" description:"         test cmswww routes"`
   102  	TokenInventory         shared.TokenInventoryCmd     `command:"tokeninventory" description:"(user) get the censorship record tokens of all proposals (passthrough)"`
   103  	UpdateUserKey          shared.UserKeyUpdateCmd      `command:"updateuserkey" description:"(user)   generate a new identity for the logged in user"`
   104  	UserDetails            UserDetailsCmd               `command:"userdetails" description:"(user)   get current cms user details"`
   105  	UserInvoices           UserInvoicesCmd              `command:"userinvoices" description:"(user)   get all invoices submitted by a specific user"`
   106  	UserSubContractors     UserSubContractorsCmd        `command:"usersubcontractors" description:"(user)   get all users that are linked to the user"`
   107  	Users                  shared.UsersCmd              `command:"users" description:"(user)   get a list of users"`
   108  	Secret                 shared.SecretCmd             `command:"secret" description:"(user)   ping politeiawww"`
   109  	VerifyTOTP             shared.UserTOTPVerifyCmd     `command:"verifytotp" description:"(user)  verify the set code for TOTP"`
   110  	Version                shared.VersionCmd            `command:"version" description:"(public) get server info and CSRF token"`
   111  	VoteDCC                VoteDCCCmd                   `command:"votedcc" description:"(user) vote for a given DCC during an all contractor vote"`
   112  	VoteDetails            VoteDetailsCmd               `command:"votedetails" description:"(user) get the details for a dcc vote"`
   113  }
   114  
   115  // signedMerkleRoot calculates the merkle root of the passed in list of files
   116  // and metadata, signs the merkle root with the passed in identity and returns
   117  // the signature.
   118  func signedMerkleRoot(files []pi.File, md []pi.Metadata, id *identity.FullIdentity) (string, error) {
   119  	if len(files) == 0 {
   120  		return "", fmt.Errorf("no proposal files found")
   121  	}
   122  	mr, err := merkleRoot(files, md)
   123  	if err != nil {
   124  		return "", err
   125  	}
   126  	sig := id.SignMessage([]byte(mr))
   127  	return hex.EncodeToString(sig[:]), nil
   128  }
   129  
   130  // verifyInvoice verifies a invoice's merkle root, author signature, and
   131  // censorship record.
   132  func verifyInvoice(p cms.InvoiceRecord, serverPubKey string) error {
   133  	if len(p.Files) > 0 {
   134  		// Verify file digests
   135  		err := validateDigests(p.Files, nil)
   136  		if err != nil {
   137  			return err
   138  		}
   139  		// Verify merkle root
   140  		mr, err := merkleRoot(p.Files, nil)
   141  		if err != nil {
   142  			return err
   143  		}
   144  		if mr != p.CensorshipRecord.Merkle {
   145  			return fmt.Errorf("merkle roots do not match")
   146  		}
   147  	}
   148  
   149  	// Verify invoice signature
   150  	pid, err := identity.PublicIdentityFromString(p.PublicKey)
   151  	if err != nil {
   152  		return err
   153  	}
   154  	sig, err := util.ConvertSignature(p.Signature)
   155  	if err != nil {
   156  		return err
   157  	}
   158  	if !pid.VerifyMessage([]byte(p.CensorshipRecord.Merkle), sig) {
   159  		return fmt.Errorf("could not verify proposal signature")
   160  	}
   161  
   162  	// Verify censorship record signature
   163  	id, err := identity.PublicIdentityFromString(serverPubKey)
   164  	if err != nil {
   165  		return err
   166  	}
   167  	s, err := util.ConvertSignature(p.CensorshipRecord.Signature)
   168  	if err != nil {
   169  		return err
   170  	}
   171  	msg := []byte(p.CensorshipRecord.Merkle + p.CensorshipRecord.Token)
   172  	if !id.VerifyMessage(msg, s) {
   173  		return fmt.Errorf("could not verify censorship record signature")
   174  	}
   175  
   176  	return nil
   177  }
   178  
   179  // promptList prompts the user with the given prefix, list of valid responses,
   180  // and default list entry to use. The function will repeat the prompt to the
   181  // user until they enter a valid response.
   182  func promptList(reader *bufio.Reader, prefix string, validResponses []string, defaultEntry string) (string, error) {
   183  	// Setup the prompt according to the parameters.
   184  	validStrings := strings.Join(validResponses, "/")
   185  	var prompt string
   186  	if defaultEntry != "" {
   187  		prompt = fmt.Sprintf("%s (%s) [%s]: ", prefix, validStrings,
   188  			defaultEntry)
   189  	} else {
   190  		prompt = fmt.Sprintf("%s (%s): ", prefix, validStrings)
   191  	}
   192  
   193  	// Prompt the user until one of the valid responses is given.
   194  	for {
   195  		fmt.Print(prompt)
   196  		reply, err := reader.ReadString('\n')
   197  		if err != nil {
   198  			return "", err
   199  		}
   200  		reply = strings.TrimSpace(strings.ToLower(reply))
   201  		if reply == "" {
   202  			reply = defaultEntry
   203  		}
   204  
   205  		for _, validResponse := range validResponses {
   206  			if reply == validResponse {
   207  				return reply, nil
   208  			}
   209  		}
   210  	}
   211  }
   212  
   213  // promptListBool prompts the user for a boolean (yes/no) with the given
   214  // prefix. The function will repeat the prompt to the user until they enter a
   215  // valid response.
   216  func promptListBool(reader *bufio.Reader, prefix string, defaultEntry string) (bool, error) {
   217  	// Setup the valid responses.
   218  	valid := []string{"n", "no", "y", "yes"}
   219  	response, err := promptList(reader, prefix, valid, defaultEntry)
   220  	if err != nil {
   221  		return false, err
   222  	}
   223  	return response == "yes" || response == "y", nil
   224  }
   225  
   226  func validateParseCSV(data []byte) (*cms.InvoiceInput, error) {
   227  	LineItemType := map[string]cms.LineItemTypeT{
   228  		"labor":   cms.LineItemTypeLabor,
   229  		"expense": cms.LineItemTypeExpense,
   230  		"misc":    cms.LineItemTypeMisc,
   231  		"sub":     cms.LineItemTypeSubHours,
   232  	}
   233  	invInput := &cms.InvoiceInput{}
   234  
   235  	// Validate that the invoice is CSV-formatted.
   236  	csvReader := csv.NewReader(strings.NewReader(string(data)))
   237  	csvReader.Comma = cms.PolicyInvoiceFieldDelimiterChar
   238  	csvReader.Comment = cms.PolicyInvoiceCommentChar
   239  	csvReader.TrimLeadingSpace = true
   240  
   241  	csvFields, err := csvReader.ReadAll()
   242  	if err != nil {
   243  		return invInput, err
   244  	}
   245  
   246  	lineItems := make([]cms.LineItemsInput, 0, len(csvFields))
   247  	// Validate that line items are the correct length and contents in
   248  	// field 4 and 5 are parsable to integers
   249  	for i, lineContents := range csvFields {
   250  		lineItem := cms.LineItemsInput{}
   251  		if len(lineContents) != cms.PolicyInvoiceLineItemCount {
   252  			return invInput,
   253  				fmt.Errorf("invalid number of line items on line: %v want: %v got: %v",
   254  					i, cms.PolicyInvoiceLineItemCount, len(lineContents))
   255  		}
   256  		hours, err := strconv.Atoi(lineContents[5])
   257  		if err != nil {
   258  			return invInput,
   259  				fmt.Errorf("invalid line item hours entered on line: %v", i)
   260  		}
   261  		cost, err := strconv.Atoi(lineContents[6])
   262  		if err != nil {
   263  			return invInput,
   264  				fmt.Errorf("invalid cost entered on line: %v", i)
   265  		}
   266  		rate, err := strconv.Atoi(lineContents[8])
   267  		if err != nil {
   268  			return invInput,
   269  				fmt.Errorf("invalid subrate hours entered on line: %v", i)
   270  		}
   271  		lineItemType, ok := LineItemType[strings.ToLower(lineContents[0])]
   272  		if !ok {
   273  			return invInput,
   274  				fmt.Errorf("invalid line item type on line: %v", i)
   275  		}
   276  
   277  		lineItem.Type = lineItemType
   278  		lineItem.Domain = lineContents[1]
   279  		lineItem.Subdomain = lineContents[2]
   280  		lineItem.Description = lineContents[3]
   281  		lineItem.ProposalToken = lineContents[4]
   282  		lineItem.SubUserID = lineContents[7]
   283  		lineItem.SubRate = uint(rate * 100)
   284  		lineItem.Labor = uint(hours * 60)
   285  		lineItem.Expenses = uint(cost * 100)
   286  		lineItems = append(lineItems, lineItem)
   287  	}
   288  	invInput.LineItems = lineItems
   289  
   290  	return invInput, nil
   291  }
   292  
   293  // verifyDCC verifies a dcc's merkle root, author signature, and censorship
   294  // record.
   295  func verifyDCC(p cms.DCCRecord, serverPubKey string) error {
   296  	files := make([]pi.File, 0, 1)
   297  	files = append(files, p.File)
   298  	// Verify digests
   299  	err := validateDigests(files, nil)
   300  	if err != nil {
   301  		return err
   302  	}
   303  	// Verify merkel root
   304  	mr, err := merkleRoot(files, nil)
   305  	if err != nil {
   306  		return err
   307  	}
   308  	if mr != p.CensorshipRecord.Merkle {
   309  		return fmt.Errorf("merkle roots do not match")
   310  	}
   311  
   312  	// Verify dcc signature
   313  	pid, err := identity.PublicIdentityFromString(p.PublicKey)
   314  	if err != nil {
   315  		return err
   316  	}
   317  	sig, err := util.ConvertSignature(p.Signature)
   318  	if err != nil {
   319  		return err
   320  	}
   321  	if !pid.VerifyMessage([]byte(p.CensorshipRecord.Merkle), sig) {
   322  		return fmt.Errorf("could not verify dcc signature")
   323  	}
   324  
   325  	// Verify censorship record signature
   326  	id, err := identity.PublicIdentityFromString(serverPubKey)
   327  	if err != nil {
   328  		return err
   329  	}
   330  	s, err := util.ConvertSignature(p.CensorshipRecord.Signature)
   331  	if err != nil {
   332  		return err
   333  	}
   334  	msg := []byte(p.CensorshipRecord.Merkle + p.CensorshipRecord.Token)
   335  	if !id.VerifyMessage(msg, s) {
   336  		return fmt.Errorf("could not verify censorship record signature")
   337  	}
   338  
   339  	return nil
   340  }
   341  
   342  func _main() error {
   343  	// Load config
   344  	_cfg, err := shared.LoadConfig(defaultHomeDir,
   345  		defaultDataDirname, defaultConfigFilename)
   346  	if err != nil {
   347  		return fmt.Errorf("loading config: %v", err)
   348  	}
   349  
   350  	// Load client
   351  	_client, err := shared.NewClient(_cfg)
   352  	if err != nil {
   353  		return fmt.Errorf("loading client: %v", err)
   354  	}
   355  
   356  	// Setup global variables for cmswww commands
   357  	cfg = _cfg
   358  	client = _client
   359  
   360  	// Setup global variables for shared commands
   361  	shared.SetConfig(_cfg)
   362  	shared.SetClient(_client)
   363  
   364  	// Get politeiawww CSRF token
   365  	if cfg.CSRF == "" {
   366  		_, err := client.Version()
   367  		if err != nil {
   368  			var e *url.Error
   369  			if !errors.As(err, &e) {
   370  				// A url error likely means that politeiawww is not
   371  				// running. The user may just be trying to print the
   372  				// help message so only return an error if its not
   373  				// a url error.
   374  				return fmt.Errorf("Version: %v", err)
   375  			}
   376  		}
   377  	}
   378  
   379  	// Parse subcommand and execute
   380  	var cli cmswww
   381  	var parser = flags.NewParser(&cli, flags.Default)
   382  	if _, err := parser.Parse(); err != nil {
   383  		var flagsErr *flags.Error
   384  		if errors.As(err, &flagsErr) && flagsErr.Type == flags.ErrHelp {
   385  			os.Exit(0)
   386  		} else {
   387  			os.Exit(1)
   388  		}
   389  	}
   390  
   391  	return nil
   392  }
   393  
   394  func main() {
   395  	err := _main()
   396  	if err != nil {
   397  		fmt.Fprintf(os.Stderr, "%v\n", err)
   398  		os.Exit(1)
   399  	}
   400  }