From 9fe258ca287d3b5a898ecc290eeda2413f8414b8 Mon Sep 17 00:00:00 2001 From: Alex Yocom-Piatt Date: Wed, 1 Jul 2020 16:41:05 -0500 Subject: [PATCH] flush out proposal owner approve invoice --- mdstream/mdstream.go | 77 ++++++++++++++++++++++------ politeiawww/api/cms/v1/v1.go | 15 ++++++ politeiawww/cmswww.go | 35 +++++++++++++ politeiawww/invoices.go | 99 ++++++++++++++++++++++++++++++++++-- 4 files changed, 206 insertions(+), 20 deletions(-) diff --git a/mdstream/mdstream.go b/mdstream/mdstream.go index 85d3058123..d26c8863b2 100644 --- a/mdstream/mdstream.go +++ b/mdstream/mdstream.go @@ -20,28 +20,30 @@ import ( const ( // mdstream IDs - IDProposalGeneral = 0 - IDRecordStatusChange = 2 - IDInvoiceGeneral = 3 - IDInvoiceStatusChange = 4 - IDInvoicePayment = 5 - IDDCCGeneral = 6 - IDDCCStatusChange = 7 - IDDCCSupportOpposition = 8 + IDProposalGeneral = 0 + IDRecordStatusChange = 2 + IDInvoiceGeneral = 3 + IDInvoiceStatusChange = 4 + IDInvoicePayment = 5 + IDDCCGeneral = 6 + IDDCCStatusChange = 7 + IDDCCSupportOpposition = 8 + IDInvoiceProposalApprove = 9 // Note that 13 is in use by the decred plugin // Note that 14 is in use by the decred plugin // Note that 15 is in use by the decred plugin // mdstream current supported versions - VersionProposalGeneral = 2 - VersionRecordStatusChange = 2 - VersionInvoiceGeneral = 1 - VersionInvoiceStatusChange = 1 - VersionInvoicePayment = 1 - VersionDCCGeneral = 1 - VersionDCCStatusChange = 1 - VersionDCCSupposeOpposition = 1 + VersionProposalGeneral = 2 + VersionRecordStatusChange = 2 + VersionInvoiceGeneral = 1 + VersionInvoiceStatusChange = 1 + VersionInvoicePayment = 1 + VersionDCCGeneral = 1 + VersionDCCStatusChange = 1 + VersionDCCSupposeOpposition = 1 + VersionInvoiceProposalApprove = 1 // Filenames of user defined metadata that is stored as politeiad // files instead of politeiad metadata streams. This is done so @@ -530,3 +532,46 @@ func DecodeDCCSupportOpposition(payload []byte) ([]DCCSupportOpposition, error) return md, nil } + +// InvoiceProposalApprove represents an invoice status change and is stored +// in the metadata IDInvoiceProposalApprove in politeiad. +type InvoiceProposalApprove struct { + Version uint `json:"version"` // Version of the struct + PublicKey string `json:"publickey"` // Identity of the administrator + Signature string `json:"signature"` // Signature of the line item payload included + Token string `json:"token"` // Token of the invoice + Timestamp int64 `json:"timestamp"` + LineItems []byte `json:"lineitems"` // json payload of line items that are being approved +} + +// EncodeInvoiceProposalApprove encodes a InvoiceProposalApprove into a +// JSON byte slice. +func EncodeInvoiceProposalApprove(md InvoiceProposalApprove) ([]byte, error) { + b, err := json.Marshal(md) + if err != nil { + return nil, err + } + + return b, nil +} + +// DecodeInvoiceProposalApprove decodes a JSON byte slice into a slice of +// InvoiceProposalApproves. +func DecodeInvoiceProposalApprove(payload []byte) ([]InvoiceProposalApprove, error) { + var md []InvoiceProposalApprove + + d := json.NewDecoder(strings.NewReader(string(payload))) + for { + var m InvoiceProposalApprove + err := d.Decode(&m) + if err == io.EOF { + break + } else if err != nil { + return nil, err + } + + md = append(md, m) + } + + return md, nil +} diff --git a/politeiawww/api/cms/v1/v1.go b/politeiawww/api/cms/v1/v1.go index 502832b6c0..f47c6a79e4 100644 --- a/politeiawww/api/cms/v1/v1.go +++ b/politeiawww/api/cms/v1/v1.go @@ -54,6 +54,7 @@ const ( RouteProposalBilling = "/proposals/billing" RouteProposalBillingSummary = "/proposals/spendingsummary" RouteProposalBillingDetails = "/proposals/spendingdetails" + RouteProposalInvoiceApprove = "/proposals/approve" // Invoice status codes InvoiceStatusInvalid InvoiceStatusT = 0 // Invalid status @@ -1044,3 +1045,17 @@ type ProposalBillingDetails struct { type ProposalBillingDetailsReply struct { Details ProposalSpending `json:"details"` } + +// ProposalOwnerApprove is used to approve or reject an proposal referenced +// invoice. +type ProposalOwnerApprove struct { + Token string `json:"token"` + Status InvoiceStatusT `json:"status"` + LineItems []LineItemsInput `json:"lineitems"` + Signature string `json:"signature"` // Signature of LineItems Approved + PublicKey string `json:"publickey"` // Public key of admin +} + +// ProposalOwnerApprove used to reply to a ProposalOwnerApprove command. +type ProposalOwnerApproveReply struct { +} diff --git a/politeiawww/cmswww.go b/politeiawww/cmswww.go index fc42057173..91a1e901fe 100644 --- a/politeiawww/cmswww.go +++ b/politeiawww/cmswww.go @@ -1094,6 +1094,38 @@ func (p *politeiawww) makeProposalsRequest(method string, route string, v interf return responseBody, nil } +// handleProposalInvoiceApprove handles request for proposal owners to approve +// an invoices' line items that reference their proposal. +func (p *politeiawww) handleProposalInvoiceApprove(w http.ResponseWriter, r *http.Request) { + log.Tracef("handleProposalInvoiceApprove") + + var poa cms.ProposalOwnerApprove + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&poa); err != nil { + RespondWithError(w, r, 0, "handleProposalInvoiceApprove: unmarshal", + www.UserError{ + ErrorCode: www.ErrorStatusInvalidInput, + }) + return + } + + user, err := p.getSessionUser(w, r) + if err != nil { + RespondWithError(w, r, 0, + "handleProposalInvoiceApprove: getSessionUser %v", err) + return + } + + poar, err := p.processProposalInvoiceApprove(poa, user) + if err != nil { + RespondWithError(w, r, 0, + "handleProposalInvoiceApprove: processStartVoteDCC %v", err) + return + } + + util.RespondWithJSON(w, http.StatusOK, poar) +} + func (p *politeiawww) setCMSWWWRoutes() { // Templates //p.addTemplate(templateNewProposalSubmittedName, @@ -1184,6 +1216,9 @@ func (p *politeiawww) setCMSWWWRoutes() { p.addRoute(http.MethodPost, www.PoliteiaWWWAPIRoute, www.RouteBatchProposals, p.handlePassThroughBatchProposals, permissionLogin) + p.addRoute(http.MethodPost, cms.APIRoute, + cms.RouteProposalInvoiceApprove, p.handleProposalInvoiceApprove, + permissionLogin) // Unauthenticated websocket p.addRoute("", www.PoliteiaWWWAPIRoute, diff --git a/politeiawww/invoices.go b/politeiawww/invoices.go index 8e79ac94ea..bd7af23df2 100644 --- a/politeiawww/invoices.go +++ b/politeiawww/invoices.go @@ -1968,13 +1968,11 @@ func (p *politeiawww) processProposalBillingSummary(pbs cms.ProposalBillingSumma if err != nil { return nil, err } - var tvr www.TokenInventoryReply err = json.Unmarshal(data, &tvr) if err != nil { return nil, err } - approvedProposals := tvr.Approved approvedProposalDetails := make([]www.ProposalRecord, 0, len(approvedProposals)) @@ -2074,7 +2072,6 @@ func (p *politeiawww) processProposalBillingDetails(pbd cms.ProposalBillingDetai if err != nil { return nil, err } - spendingSummary := cms.ProposalSpending{} spendingSummary.Token = pbd.Token @@ -2104,7 +2101,6 @@ func (p *politeiawww) processProposalBillingDetails(pbd cms.ProposalBillingDetai if err != nil { return nil, err } - var pdr www.ProposalDetailsReply err = json.Unmarshal(data, &pdr) if err != nil { @@ -2118,3 +2114,98 @@ func (p *politeiawww) processProposalBillingDetails(pbd cms.ProposalBillingDetai reply.Details = spendingSummary return reply, nil } + +// processProposalInvoiceApprove appends a proposal owners approval onto the +// invoice records metadata which will allow admins to determine if an invoice +// is fully approved. +func (p *politeiawww) processProposalInvoiceApprove(poa cms.ProposalOwnerApprove, u *user.User) (*cms.ProposalOwnerApproveReply, error) { + invRec, err := p.getInvoice(poa.Token) + if err != nil { + return nil, err + } + cmsUser, err := p.getCMSUserByID(u.ID.String()) + if err != nil { + return nil, err + } + proposalFound := false + for _, lineItem := range invRec.Input.LineItems { + // If the proposal token is empty then don't display it for the + // non invoice owner or admin. + if lineItem.ProposalToken == "" { + continue + } + // Check to see that proposal token matches an owned proposal by + // the recommending user, if not don't include the line item in the + // list of line items to display. + if stringInSlice(cmsUser.ProposalsOwned, lineItem.ProposalToken) { + proposalFound = true + } + } + if !proposalFound { + err := www.UserError{ + ErrorCode: www.ErrorStatusUserActionNotAllowed, + } + return nil, err + } + + b, err := json.Marshal(poa.LineItems) + if err != nil { + return nil, err + } + + // Validate signature + msg := fmt.Sprintf("%v", b) + err = validateSignature(poa.PublicKey, poa.Signature, msg) + if err != nil { + return nil, err + } + + // Create the change record. + c := mdstream.InvoiceProposalApprove{ + Version: mdstream.VersionInvoiceProposalApprove, + PublicKey: u.PublicKey(), + Timestamp: time.Now().Unix(), + Signature: poa.Signature, + Token: poa.Token, + LineItems: b, + } + blob, err := mdstream.EncodeInvoiceProposalApprove(c) + if err != nil { + return nil, err + } + challenge, err := util.Random(pd.ChallengeSize) + if err != nil { + return nil, err + } + + pdCommand := pd.UpdateVettedMetadata{ + Challenge: hex.EncodeToString(challenge), + Token: poa.Token, + MDAppend: []pd.MetadataStream{ + { + ID: mdstream.IDInvoiceProposalApprove, + Payload: string(blob), + }, + }, + } + + responseBody, err := p.makeRequest(http.MethodPost, pd.UpdateVettedMetadataRoute, pdCommand) + if err != nil { + return nil, err + } + + var pdReply pd.UpdateVettedMetadataReply + err = json.Unmarshal(responseBody, &pdReply) + if err != nil { + return nil, fmt.Errorf("Could not unmarshal UpdateVettedMetadataReply: %v", + err) + } + + // Verify the UpdateVettedMetadata challenge. + err = util.VerifyChallenge(p.cfg.Identity, challenge, pdReply.Response) + if err != nil { + return nil, err + } + + return &cms.ProposalOwnerApproveReply{}, nil +}