diff --git a/politeiawww/api/cms/v1/v1.go b/politeiawww/api/cms/v1/v1.go index 627e04f449..ae56565a33 100644 --- a/politeiawww/api/cms/v1/v1.go +++ b/politeiawww/api/cms/v1/v1.go @@ -235,6 +235,7 @@ const ( ErrorStatusDCCVoteEnded www.ErrorStatusT = 1054 ErrorStatusDCCVoteStillLive www.ErrorStatusT = 1055 ErrorStatusDCCDuplicateVote www.ErrorStatusT = 1056 + ErrorStatusInvalidManageUserAction www.ErrorStatusT = 1057 ProposalsMainnet = "https://proposals.decred.org" ProposalsTestnet = "https://test-proposals.decred.org" @@ -318,6 +319,14 @@ var ( }, } + // PolicyCMSManageUserActions defines the actions allowed to edit the + // user fields on manage user route + PolicyCMSManageUserActions = []string{ + "", // Do nothing + "set", // Set payload + "reset", // Reset + } + // ErrorStatus converts error status codes to human readable text. ErrorStatus = map[www.ErrorStatusT]string{ ErrorStatusMalformedName: "malformed name", @@ -374,6 +383,7 @@ var ( ErrorStatusDCCVoteEnded: "the all contractor voting period has ended", ErrorStatusDCCVoteStillLive: "cannot update status of a DCC while a vote is still live", ErrorStatusDCCDuplicateVote: "user has already submitted a vote for the given dcc", + ErrorStatusInvalidManageUserAction: "the client selected an invalid manage user action", } ) @@ -529,6 +539,7 @@ type PolicyReply struct { CMSStatementSupportedChars []string `json:"cmsstatementsupportedchars"` CMSSupportedLineItemTypes []AvailableLineItemType `json:"supportedlineitemtypes"` CMSSupportedDomains []AvailableDomain `json:"supporteddomains"` + CMSManageUserActions []string `json:"cmsmanageuseractions"` } // UserInvoices is used to get all of the invoices by userID. @@ -700,13 +711,20 @@ type EditUser struct { // EditUserReply is the reply for the EditUser command. type EditUserReply struct{} +// CMSManageUserAction specifies commands that can be used to manage +// a user field. +type CMSManageUserAction struct { + Action string // Set or reset + Payload []string +} + // CMSManageUser updates the various fields for a given user. type CMSManageUser struct { - UserID string `json:"userid"` - Domain DomainTypeT `json:"domain,omitempty"` - ContractorType ContractorTypeT `json:"contractortype,omitempty"` - SupervisorUserIDs []string `json:"supervisoruserids,omitempty"` - ProposalsOwned []string `json:"proposalsowned,omitempty"` + UserID string `json:"userid"` + Domain DomainTypeT `json:"domain,omitempty"` + ContractorType ContractorTypeT `json:"contractortype,omitempty"` + SupervisorUserIDs CMSManageUserAction `json:"supervisoruserids,omitempty"` + ProposalsOwned CMSManageUserAction `json:"proposalsowned,omitempty"` } // CMSManageUserReply is the reply for the CMSManageUserReply command. diff --git a/politeiawww/cmd/cmswww/manageuser.go b/politeiawww/cmd/cmswww/manageuser.go index 548108c329..944c345e98 100644 --- a/politeiawww/cmd/cmswww/manageuser.go +++ b/politeiawww/cmd/cmswww/manageuser.go @@ -5,10 +5,12 @@ package main import ( + "encoding/hex" "fmt" "strconv" "strings" + pd "github.com/decred/politeia/politeiad/api/v1" cms "github.com/decred/politeia/politeiawww/api/cms/v1" "github.com/decred/politeia/politeiawww/cmd/shared" "github.com/google/uuid" @@ -43,8 +45,10 @@ func (cmd *CMSManageUserCmd) Execute(args []string) error { "revoked": cms.ContractorTypeRevoked, } + userID := cmd.Args.UserID + // Validate user ID - _, err := uuid.Parse(cmd.Args.UserID) + _, err := uuid.Parse(userID) if err != nil { return fmt.Errorf("invalid user ID: %v", err) } @@ -86,21 +90,41 @@ func (cmd *CMSManageUserCmd) Execute(args []string) error { } // Validate supervisor user IDs - supervisorIDs := make([]string, 0, 16) - if cmd.SupervisorUserIDs != "" { - supervisorIDs = strings.Split(cmd.SupervisorUserIDs, ",") - for _, v := range supervisorIDs { - _, err := uuid.Parse(v) + var manageSupervisors cms.CMSManageUserAction + switch cmd.SupervisorUserIDs { + case "": + manageSupervisors.Action = cmd.SupervisorUserIDs + case "reset": + manageSupervisors.Action = cmd.SupervisorUserIDs + default: + ids := strings.Split(cmd.SupervisorUserIDs, ",") + for _, id := range ids { + _, err := uuid.Parse(id) if err != nil { - return fmt.Errorf("invalid supervisor ID '%v': %v", v, err) + return fmt.Errorf("invalid supervisor ID '%v': %v", id, err) } } + manageSupervisors.Action = "set" + manageSupervisors.Payload = ids } - // Validate supervisor user IDs - proposalsOwned := make([]string, 0, 16) - if cmd.ProposalsOwned != "" { - proposalsOwned = strings.Split(cmd.ProposalsOwned, ",") + // Validate proposals owned + var manageProposalsOwned cms.CMSManageUserAction + switch cmd.ProposalsOwned { + case "": + manageProposalsOwned.Action = cmd.ProposalsOwned + case "reset": + manageProposalsOwned.Action = cmd.ProposalsOwned + default: + tokens := strings.Split(cmd.ProposalsOwned, ",") + for _, token := range tokens { + b, err := hex.DecodeString(token) + if err != nil || len(b) != pd.TokenSize { + return fmt.Errorf("invalid proposal token '%v': %v", token, err) + } + } + manageProposalsOwned.Action = "set" + manageProposalsOwned.Payload = tokens } // Send request @@ -108,8 +132,8 @@ func (cmd *CMSManageUserCmd) Execute(args []string) error { UserID: cmd.Args.UserID, Domain: domain, ContractorType: contractorType, - SupervisorUserIDs: supervisorIDs, - ProposalsOwned: proposalsOwned, + SupervisorUserIDs: manageSupervisors, + ProposalsOwned: manageProposalsOwned, } err = shared.PrintJSON(mu) if err != nil { diff --git a/politeiawww/cmsuser.go b/politeiawww/cmsuser.go index fd8ea59d87..72d917cd3d 100644 --- a/politeiawww/cmsuser.go +++ b/politeiawww/cmsuser.go @@ -8,6 +8,7 @@ import ( "strings" "time" + pd "github.com/decred/politeia/politeiad/api/v1" cms "github.com/decred/politeia/politeiawww/api/cms/v1" www "github.com/decred/politeia/politeiawww/api/www/v1" "github.com/decred/politeia/politeiawww/user" @@ -362,40 +363,83 @@ func (p *politeiawww) processManageCMSUser(mu cms.CMSManageUser) (*cms.CMSManage if mu.ContractorType != 0 { uu.ContractorType = int(mu.ContractorType) } - if len(mu.SupervisorUserIDs) > 0 { - // Validate SupervisorUserID input - parseSuperUserIds := make([]uuid.UUID, 0, len(mu.SupervisorUserIDs)) - for _, super := range mu.SupervisorUserIDs { - parseUUID, err := uuid.Parse(super) + + previousCMSUser, err := p.getCMSUserByIDRaw(mu.UserID) + if err != nil { + return nil, err + } + + // Manage a user's supervisors + switch mu.SupervisorUserIDs.Action { + case "set": + supervisors := mu.SupervisorUserIDs.Payload + parseSupervisors := make([]uuid.UUID, 0, len(supervisors)) + for _, id := range supervisors { + // Check if uuid is valid + parseUUID, err := uuid.Parse(id) if err != nil { - e := fmt.Sprintf("invalid uuid: %v", super) + e := fmt.Sprintf("invalid uuid: %v", id) return nil, www.UserError{ ErrorCode: cms.ErrorStatusInvalidSupervisorUser, ErrorContext: []string{e}, } } - u, err := p.getCMSUserByID(super) + // Check if user exists + u, err := p.getCMSUserByID(id) if err != nil { - e := fmt.Sprintf("user not found: %v", super) + e := fmt.Sprintf("user not found: %v", id) return nil, www.UserError{ ErrorCode: cms.ErrorStatusInvalidSupervisorUser, ErrorContext: []string{e}, } } + // Make sure the user is a supervisor if u.ContractorType != cms.ContractorTypeSupervisor { - e := fmt.Sprintf("user not a supervisor: %v", super) + e := fmt.Sprintf("user not a supervisor: %v", id) return nil, www.UserError{ ErrorCode: cms.ErrorStatusInvalidSupervisorUser, ErrorContext: []string{e}, } } - parseSuperUserIds = append(parseSuperUserIds, parseUUID) + parseSupervisors = append(parseSupervisors, parseUUID) + } + uu.SupervisorUserIDs = parseSupervisors + case "reset": + uu.SupervisorUserIDs = []uuid.UUID{} + case "": + uu.SupervisorUserIDs = previousCMSUser.SupervisorUserIDs + default: + e := fmt.Sprintf("invalid action: %v", mu.SupervisorUserIDs.Action) + return nil, www.UserError{ + ErrorCode: cms.ErrorStatusInvalidManageUserAction, + ErrorContext: []string{e}, } - uu.SupervisorUserIDs = parseSuperUserIds } - if len(mu.ProposalsOwned) > 0 { - uu.ProposalsOwned = mu.ProposalsOwned + // Manage a user's owned proposal + switch mu.ProposalsOwned.Action { + case "set": + tokens := mu.ProposalsOwned.Payload + for _, token := range tokens { + b, err := hex.DecodeString(token) + if err != nil || len(b) != pd.TokenSize { + return nil, www.UserError{ + ErrorCode: cms.ErrorStatusMalformedProposalToken, + ErrorContext: []string{token}, + } + } + } + uu.ProposalsOwned = tokens + case "reset": + uu.ProposalsOwned = []string{} + case "": + uu.ProposalsOwned = previousCMSUser.ProposalsOwned + default: + e := fmt.Sprintf("invalid action: %v", mu.ProposalsOwned.Action) + return nil, www.UserError{ + ErrorCode: cms.ErrorStatusInvalidManageUserAction, + ErrorContext: []string{e}, + } } payload, err := user.EncodeUpdateCMSUser(uu) diff --git a/politeiawww/cmswww.go b/politeiawww/cmswww.go index f9f0cedb76..68c4ceda15 100644 --- a/politeiawww/cmswww.go +++ b/politeiawww/cmswww.go @@ -411,6 +411,7 @@ func (p *politeiawww) handleCMSPolicy(w http.ResponseWriter, r *http.Request) { CMSStatementSupportedChars: cms.PolicySponsorStatementSupportedChars, CMSSupportedDomains: cms.PolicySupportedCMSDomains, CMSSupportedLineItemTypes: cms.PolicyCMSSupportedLineItemTypes, + CMSManageUserActions: cms.PolicyCMSManageUserActions, } util.RespondWithJSON(w, http.StatusOK, reply)