diff --git a/actions.go b/actions.go index fde767c..f41e1ff 100644 --- a/actions.go +++ b/actions.go @@ -10,8 +10,8 @@ func WithdrawMotion(decision *Decision, voter *Voter) (err error) { // load template, fill name, tag, title, content type mailContext struct { *Decision - Name string - Sender string + Name string + Sender string Recipient string } context := mailContext{decision, voter.Name, config.NoticeSenderAddress, config.BoardMailAddress} diff --git a/boardvoting.go b/boardvoting.go index 77499a2..1fe031e 100644 --- a/boardvoting.go +++ b/boardvoting.go @@ -1,6 +1,7 @@ package main import ( + "context" "crypto/tls" "crypto/x509" "fmt" @@ -35,7 +36,15 @@ func renderTemplate(w http.ResponseWriter, tmpl []string, context interface{}) { } } -func authenticateRequest(w http.ResponseWriter, r *http.Request, authRequired bool, handler func(http.ResponseWriter, *http.Request, *Voter)) { +type contextKey int + +const ( + ctxNeedsAuth contextKey = iota + ctxVoter contextKey = iota + ctxDecision contextKey = iota +) + +func authenticateRequest(w http.ResponseWriter, r *http.Request, handler func(http.ResponseWriter, *http.Request)) { for _, cert := range r.TLS.PeerCertificates { for _, extKeyUsage := range cert.ExtKeyUsage { if extKeyUsage == x509.ExtKeyUsageClientAuth { @@ -46,19 +55,20 @@ func authenticateRequest(w http.ResponseWriter, r *http.Request, authRequired bo return } if voter != nil { - handler(w, r, voter) + handler(w, r.WithContext(context.WithValue(r.Context(), ctxVoter, voter))) return } } } } } - if authRequired { + needsAuth, ok := r.Context().Value(ctxNeedsAuth).(bool) + if ok && needsAuth { w.WriteHeader(http.StatusForbidden) renderTemplate(w, []string{"denied.html"}, nil) return } - handler(w, r, nil) + handler(w, r) } type motionParameters struct { @@ -66,7 +76,7 @@ type motionParameters struct { } type motionListParameters struct { - Page int64 + Page int64 Flags struct { Confirmed, Withdraw, Unvoted bool } @@ -96,74 +106,86 @@ func parseMotionListParameters(r *http.Request) motionListParameters { return m } -func motionListHandler(w http.ResponseWriter, r *http.Request, voter *Voter) { +func motionListHandler(w http.ResponseWriter, r *http.Request) { params := parseMotionListParameters(r) - var context struct { + var templateContext struct { Decisions []*DecisionForDisplay Voter *Voter Params *motionListParameters PrevPage, NextPage int64 PageTitle string } - context.Voter = voter - context.Params = ¶ms + if voter, ok := r.Context().Value(ctxVoter).(*Voter); ok { + templateContext.Voter = voter + } + templateContext.Params = ¶ms var err error - if params.Flags.Unvoted { - if context.Decisions, err = FindVotersUnvotedDecisionsForDisplayOnPage(params.Page, voter); err != nil { + if params.Flags.Unvoted && templateContext.Voter != nil { + if templateContext.Decisions, err = FindVotersUnvotedDecisionsForDisplayOnPage( + params.Page, templateContext.Voter); err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } } else { - if context.Decisions, err = FindDecisionsForDisplayOnPage(params.Page); err != nil { + if templateContext.Decisions, err = FindDecisionsForDisplayOnPage(params.Page); err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } } - if len(context.Decisions) > 0 { - olderExists, err := context.Decisions[len(context.Decisions)-1].OlderExists() + if len(templateContext.Decisions) > 0 { + olderExists, err := templateContext.Decisions[len(templateContext.Decisions)-1].OlderExists() if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } if olderExists { - context.NextPage = params.Page + 1 + templateContext.NextPage = params.Page + 1 } } if params.Page > 1 { - context.PrevPage = params.Page - 1 + templateContext.PrevPage = params.Page - 1 } - renderTemplate(w, []string{"motions.html", "motion_fragments.html", "header.html", "footer.html"}, context) + renderTemplate(w, []string{"motions.html", "motion_fragments.html", "header.html", "footer.html"}, templateContext) } -func motionHandler(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay) { +func motionHandler(w http.ResponseWriter, r *http.Request) { params := parseMotionParameters(r) - var context struct { + decision, ok := getDecisionFromRequest(r) + if !ok { + http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed) + return + } + + var templateContext struct { Decision *DecisionForDisplay Voter *Voter Params *motionParameters PrevPage, NextPage int64 PageTitle string } - context.Voter = voter - context.Params = ¶ms + voter, ok := getVoterFromRequest(r) + if ok { + templateContext.Voter = voter + } + templateContext.Params = ¶ms if params.ShowVotes { if err := decision.LoadVotes(); err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } } - context.Decision = decision - context.PageTitle = fmt.Sprintf("Motion %s: %s", decision.Tag, decision.Title) - renderTemplate(w, []string{"motion.html", "motion_fragments.html", "header.html", "footer.html"}, context) + templateContext.Decision = decision + templateContext.PageTitle = fmt.Sprintf("Motion %s: %s", decision.Tag, decision.Title) + renderTemplate(w, []string{"motion.html", "motion_fragments.html", "header.html", "footer.html"}, templateContext) } -func singleDecisionHandler(w http.ResponseWriter, r *http.Request, v *Voter, tag string, handler func(http.ResponseWriter, *http.Request, *Voter, *DecisionForDisplay)) { +func singleDecisionHandler(w http.ResponseWriter, r *http.Request, tag string, handler func(http.ResponseWriter, *http.Request)) { decision, err := FindDecisionForDisplayByTag(tag) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) @@ -173,22 +195,45 @@ func singleDecisionHandler(w http.ResponseWriter, r *http.Request, v *Voter, tag http.NotFound(w, r) return } - handler(w, r, v, decision) + handler(w, r.WithContext(context.WithValue(r.Context(), ctxDecision, decision))) } -type motionsHandler struct{} - type motionActionHandler interface { - Handle(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay) + Handle(w http.ResponseWriter, r *http.Request) NeedsAuth() bool } -type withDrawMotionAction struct{} +type authenticationRequiredHandler struct{} -func (withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay) { - fmt.Fprintln(w, "Withdraw motion", decision.Tag) - // TODO: implement - if r.Method == http.MethodPost { +func (authenticationRequiredHandler) NeedsAuth() bool { + return true +} + +type withDrawMotionAction struct { + authenticationRequiredHandler +} + +func getVoterFromRequest(r *http.Request) (voter *Voter, ok bool) { + voter, ok = r.Context().Value(ctxVoter).(*Voter) + return +} + +func getDecisionFromRequest(r *http.Request) (decision *DecisionForDisplay, ok bool) { + decision, ok = r.Context().Value(ctxDecision).(*DecisionForDisplay) + return +} + +func (withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request) { + voter, voter_ok := getVoterFromRequest(r) + decision, decision_ok := getDecisionFromRequest(r) + + if !voter_ok || !decision_ok || decision.Status != voteStatusPending { + http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed) + return + } + + switch r.Method { + case http.MethodPost: if confirm, err := strconv.ParseBool(r.PostFormValue("confirm")); err != nil { log.Println("could not parse confirm parameter:", err) http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) @@ -197,23 +242,28 @@ func (withDrawMotionAction) Handle(w http.ResponseWriter, r *http.Request, voter } else { http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) } + http.Redirect(w, r, "/motions/", http.StatusTemporaryRedirect) + return + default: + fmt.Fprintln(w, "Withdraw motion", decision.Tag) } } -func (withDrawMotionAction) NeedsAuth() bool { - return true +type editMotionAction struct { + authenticationRequiredHandler } -type editMotionAction struct{} - -func (editMotionAction) Handle(w http.ResponseWriter, r *http.Request, voter *Voter, decision *DecisionForDisplay) { +func (editMotionAction) Handle(w http.ResponseWriter, r *http.Request) { + decision, ok := getDecisionFromRequest(r) + if !ok || decision.Status != voteStatusPending { + http.Error(w, http.StatusText(http.StatusPreconditionFailed), http.StatusPreconditionFailed) + return + } fmt.Fprintln(w, "Edit motion", decision.Tag) // TODO: implement } -func (editMotionAction) NeedsAuth() bool { - return true -} +type motionsHandler struct{} func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err := db.Ping(); err != nil { @@ -224,12 +274,12 @@ func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { var motionActionMap = map[string]motionActionHandler{ "withdraw": withDrawMotionAction{}, - "edit": editMotionAction{}, + "edit": editMotionAction{}, } switch { case subURL == "": - authenticateRequest(w, r, false, motionListHandler) + authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)), motionListHandler) return case strings.Count(subURL, "/") == 1: parts := strings.Split(subURL, "/") @@ -240,16 +290,18 @@ func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) return } - authenticateRequest(w, r, action.NeedsAuth(), func(w http.ResponseWriter, r *http.Request, v *Voter) { - singleDecisionHandler(w, r, v, motionTag, action.Handle) - }) - + authenticateRequest( + w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, action.NeedsAuth())), + func(w http.ResponseWriter, r *http.Request) { + singleDecisionHandler(w, r, motionTag, action.Handle) + }) logger.Printf("motion: %s, action: %s\n", motionTag, action) return case strings.Count(subURL, "/") == 0: - authenticateRequest(w, r, false, func(w http.ResponseWriter, r *http.Request, v *Voter) { - singleDecisionHandler(w, r, v, subURL, motionHandler) - }) + authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, false)), + func(w http.ResponseWriter, r *http.Request) { + singleDecisionHandler(w, r, subURL, motionHandler) + }) return default: fmt.Fprintf(w, "No handler for '%s'", subURL) @@ -257,8 +309,10 @@ func (h motionsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } } -func newMotionHandler(w http.ResponseWriter, _ *http.Request, _ *Voter) { - fmt.Fprintln(w,"New motion") +func newMotionHandler(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, "New motion") + voter, _ := getVoterFromRequest(r) + fmt.Fprintf(w, "%+v\n", voter) // TODO: implement } @@ -301,7 +355,7 @@ func main() { http.Handle("/motions/", http.StripPrefix("/motions/", motionsHandler{})) http.HandleFunc("/newmotion/", func(w http.ResponseWriter, r *http.Request) { - authenticateRequest(w, r, true, newMotionHandler) + authenticateRequest(w, r.WithContext(context.WithValue(r.Context(), ctxNeedsAuth, true)), newMotionHandler) }) http.Handle("/static/", http.FileServer(http.Dir("."))) http.Handle("/", http.RedirectHandler("/motions/", http.StatusMovedPermanently)) @@ -319,7 +373,7 @@ func main() { // setup HTTPS server tlsConfig := &tls.Config{ ClientCAs: caCertPool, - ClientAuth: tls.RequireAndVerifyClientCert, + ClientAuth: tls.VerifyClientCertIfGiven, } tlsConfig.BuildNameToCertificate() diff --git a/models.go b/models.go index fe6cd23..46d669e 100644 --- a/models.go +++ b/models.go @@ -25,7 +25,7 @@ FROM decisions JOIN voters ON decisions.proponent=voters.id WHERE decisions.tag=$1;` sqlGetVoter = ` -SELECT voters.id, voters.name +SELECT voters.id, voters.name, voters.enabled, voters.reminder FROM voters JOIN emails ON voters.id=emails.voter WHERE emails.address=$1 AND voters.enabled=1` @@ -139,15 +139,22 @@ func (v VoteChoice) String() string { } } +const ( + voteStatusDeclined = -1 + voteStatusPending = 0 + voteStatusApproved = 1 + voteStatusWithdrawn = -2 +) + func (v VoteStatus) String() string { switch v { - case -1: + case voteStatusDeclined: return "declined" - case 0: + case voteStatusPending: return "pending" - case 1: + case voteStatusApproved: return "approved" - case -2: + case voteStatusWithdrawn: return "withdrawn" default: return "unknown" @@ -240,7 +247,7 @@ func FindVotersUnvotedDecisionsForDisplayOnPage(page int64, voter *Voter) (decis } defer decisionsStmt.Close() - rows, err := decisionsStmt.Queryx(page - 1, voter.Id) + rows, err := decisionsStmt.Queryx(page-1, voter.Id) if err != nil { logger.Printf("Error loading motions for page %d: %v\n", page, err) return