diff --git a/README.md b/README.md index 6a7323b5..037b3f70 100644 --- a/README.md +++ b/README.md @@ -176,6 +176,7 @@ _this is the recommended way to run remark42_ | max-comment | MAX_COMMENT_SIZE | `2048` | comment's size limit | | max-votes | MAX_VOTES | `-1` | votes limit per comment, `-1` - unlimited | | votes-ip | VOTES_IP | `false` | restrict votes from the same ip | +| anon-vote | ANON_VOTE | `false` | allow voting for anonymous users, require VOTES_IP to be enabled as well | | votes-ip-time | VOTES_IP_TIME | `5m` | same ip vote restriction time, `0s` - unlimited | | low-score | LOW_SCORE | `-5` | low score threshold | | critical-score | CRITICAL_SCORE | `-10` | critical score threshold | diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index 2d7710ce..59082c0e 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -52,6 +52,7 @@ type ServerCommand struct { Stream StreamGroup `group:"stream" namespace:"stream" env-namespace:"STREAM"` Sites []string `long:"site" env:"SITE" default:"remark" description:"site names" env-delim:","` + AnonymousVote bool `long:"anon-vote" env:"ANON_VOTE" description:"enable anonymous votes (works only with VOTES_IP enabled)"` AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" default:"" description:"admin basic auth password"` BackupLocation string `long:"backup" env:"BACKUP_PATH" default:"./var/backup" description:"backups location"` MaxBackupFiles int `long:"max-back" env:"MAX_BACKUP_FILES" default:"10" description:"max backups to keep"` @@ -367,6 +368,7 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) { MaxActive: int32(s.Stream.MaxActive), }, EmojiEnabled: s.EnableEmoji, + AnonVote: s.AnonymousVote && s.RestrictVoteIP, SimpleView: s.SimpleView, } diff --git a/backend/app/rest/api/rest.go b/backend/app/rest/api/rest.go index 55072b4b..535a6c89 100644 --- a/backend/app/rest/api/rest.go +++ b/backend/app/rest/api/rest.go @@ -46,6 +46,7 @@ type Rest struct { ImageService *image.Service Streamer *Streamer + AnonVote bool WebRoot string RemarkURL string ReadOnlyAge int @@ -308,7 +309,7 @@ func (s *Rest) routes() chi.Router { rauth.Put("/comment/{id}", s.privRest.updateCommentCtrl) rauth.Post("/comment", s.privRest.createCommentCtrl) - rauth.With(rejectAnonUser).Put("/vote/{id}", s.privRest.voteCtrl) + rauth.Put("/vote/{id}", s.privRest.voteCtrl) rauth.With(rejectAnonUser).Post("/deleteme", s.privRest.deleteMeCtrl) rauth.With(rejectAnonUser).Get("/email", s.privRest.getEmailCtrl) rauth.With(rejectAnonUser).Post("/email/subscribe", s.privRest.sendEmailConfirmationCtrl) @@ -361,6 +362,7 @@ func (s *Rest) controllerGroups() (public, private, admin, rss) { authenticator: s.Authenticator, notifyService: s.NotifyService, remarkURL: s.RemarkURL, + anonVote: s.AnonVote, } admGrp := admin{ @@ -402,6 +404,7 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) { Admins []string `json:"admins"` AdminEmail string `json:"admin_email"` Auth []string `json:"auth_providers"` + AnonVote bool `json:"anon_vote"` LowScore int `json:"low_score"` CriticalScore int `json:"critical_score"` PositiveScore bool `json:"positive_score"` @@ -421,6 +424,7 @@ func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) { ReadOnlyAge: s.ReadOnlyAge, MaxImageSize: s.ImageService.Store.SizeLimit(), EmojiEnabled: s.EmojiEnabled, + AnonVote: s.AnonVote, SimpleView: s.SimpleView, } diff --git a/backend/app/rest/api/rest_private.go b/backend/app/rest/api/rest_private.go index 782b241f..11e30531 100644 --- a/backend/app/rest/api/rest_private.go +++ b/backend/app/rest/api/rest_private.go @@ -36,6 +36,7 @@ type private struct { notifyService *notify.Service authenticator *auth.Service remarkURL string + anonVote bool } type privStore interface { @@ -191,6 +192,10 @@ func (s *private) userInfoCtrl(w http.ResponseWriter, r *http.Request) { // PUT /vote/{id}?site=siteID&url=post-url&vote=1 - vote for/against comment func (s *private) voteCtrl(w http.ResponseWriter, r *http.Request) { user := rest.MustGetUserInfo(r) + if !s.anonVote && strings.HasPrefix(user.ID, "anonymous_") { + http.Error(w, "Access denied", http.StatusForbidden) + return + } locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} id := chi.URLParam(r, "id") log.Printf("[DEBUG] vote for comment %s", id) diff --git a/backend/app/rest/api/rest_private_test.go b/backend/app/rest/api/rest_private_test.go index 157dfef8..8ed68ff1 100644 --- a/backend/app/rest/api/rest_private_test.go +++ b/backend/app/rest/api/rest_private_test.go @@ -454,6 +454,57 @@ func TestRest_Vote(t *testing.T) { assert.Equal(t, map[string]bool(nil), cr.Votes) } +func TestRest_AnonVote(t *testing.T) { + ts, srv, teardown := startupT(t) + defer teardown() + + c1 := store.Comment{Text: "test test #1", + Locator: store.Locator{SiteID: "remark42", URL: "https://radio-t.com/blah"}} + c2 := store.Comment{Text: "test test #2", ParentID: "p1", + Locator: store.Locator{SiteID: "remark42", URL: "https://radio-t.com/blah"}} + + id1 := addComment(t, c1, ts) + addComment(t, c2, ts) + + vote := func(val int) int { + client := http.Client{} + req, err := http.NewRequest(http.MethodPut, + fmt.Sprintf("%s/api/v1/vote/%s?site=remark42&url=https://radio-t.com/blah&vote=%d", ts.URL, id1, val), nil) + assert.Nil(t, err) + req.Header.Add("X-JWT", anonToken) + resp, err := client.Do(req) + assert.Nil(t, err) + return resp.StatusCode + } + + getWithAnonAuth := func(url string) (body string, code int) { + client := &http.Client{Timeout: 5 * time.Second} + req, err := http.NewRequest("GET", url, nil) + require.Nil(t, err) + req.Header.Add("X-JWT", anonToken) + r, err := client.Do(req) + require.Nil(t, err) + defer r.Body.Close() + b, err := ioutil.ReadAll(r.Body) + assert.Nil(t, err) + return string(b), r.StatusCode + } + + assert.Equal(t, 403, vote(1), "vote is disallowed with anonVote false") + srv.privRest.anonVote = true + assert.Equal(t, 200, vote(1), "first vote allowed") + assert.Equal(t, 400, vote(1), "second vote rejected") + body, code := getWithAnonAuth(fmt.Sprintf("%s/api/v1/id/%s?site=remark42&url=https://radio-t.com/blah", ts.URL, id1)) + + assert.Equal(t, 200, code) + cr := store.Comment{} + err := json.Unmarshal([]byte(body), &cr) + assert.Nil(t, err) + assert.Equal(t, 1, cr.Score) + assert.Equal(t, 1, cr.Vote) + assert.Equal(t, map[string]bool(nil), cr.Votes) +} + func TestRest_Email(t *testing.T) { ts, srv, teardown := startupT(t) defer teardown() diff --git a/backend/app/rest/api/rest_test.go b/backend/app/rest/api/rest_test.go index ee2dc6ac..5aa578c6 100644 --- a/backend/app/rest/api/rest_test.go +++ b/backend/app/rest/api/rest_test.go @@ -42,6 +42,8 @@ var getStartedHTML = os.TempDir() + "/getstarted.html" var devToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImRldmVsb3BlciBvbmUiLCJpZCI6ImRldiIsInBpY3R1cmUiOiJodHRwOi8vZXhhbXBsZS5jb20vcGljLnBuZyIsImlwIjoiMTI3LjAuMC4xIiwiZW1haWwiOiJtZUBleGFtcGxlLmNvbSJ9fQ.aKUAXiZxXypgV7m1wEOgUcyPOvUDXHDi3A06YWKbcLg` +var anonToken = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImFub255bW91cyB0ZXN0IHVzZXIiLCJpZCI6ImFub255bW91c190ZXN0X3VzZXIiLCJwaWN0dXJlIjoiaHR0cDovL2V4YW1wbGUuY29tL3BpYy5wbmciLCJpcCI6IjEyNy4wLjAuMSIsImVtYWlsIjoiYW5vbkBleGFtcGxlLmNvbSJ9fQ.gAae2WMxZNZE5ebVboptPEyQ7Nk6EQxciNnGJ_mPOuU` + var devTokenBadAud = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0Ml9iYWQiLCJleHAiOjM3ODkxOTE4MjIsImp0aSI6InJhbmRvbSBpZCIsImlzcyI6InJlbWFyazQyIiwibmJmIjoxNTIxODg0MjIyLCJ1c2VyIjp7Im5hbWUiOiJkZXZlbG9wZXIgb25lIiwiaWQiOiJkZXYiLCJwaWN0dXJlIjoiaHR0cDovL2V4YW1wbGUuY29tL3BpYy5wbmciLCJpcCI6IjEyNy4wLjAuMSIsImVtYWlsIjoibWVAZXhhbXBsZS5jb20ifX0.FuTTocVtcxr4VjpfIICvU2yOb3su28VkDzj94H9Q3xY` var adminUmputunToken = `eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6MTk1NDU5Nzk4MCwianRpIjoiOTdhMmUwYWM0ZGM3ZDVmNjkyNmQ1ZTg2MjBhY2VmOWE0MGMwIiwiaWF0IjoxNDU0NTk3NjgwLCJpc3MiOiJyZW1hcms0MiIsInVzZXIiOnsibmFtZSI6IlVtcHV0dW4iLCJpZCI6ImdpdGh1Yl9lZjBmNzA2YTciLCJwaWN0dXJlIjoiaHR0cHM6Ly9yZW1hcms0Mi5yYWRpby10LmNvbS9hcGkvdjEvYXZhdGFyL2NiNDJmZjQ5M2FkZTY5NmQ4OGEzYTU5MGYxMzZhZTllMzRkZTdjMWIuaW1hZ2UiLCJhdHRycyI6eyJhZG1pbiI6dHJ1ZSwiYmxvY2tlZCI6ZmFsc2V9fX0.dZiOjWHguo9f42XCMooMcv4EmYFzifl_-LEvPZHCtks` diff --git a/compose-dev-backend.yml b/compose-dev-backend.yml index 0009d6db..25868240 100644 --- a/compose-dev-backend.yml +++ b/compose-dev-backend.yml @@ -51,6 +51,7 @@ services: - NOTIFY_EMAIL_PORT - NOTIFY_EMAIL_TLS - EMOJI=true + - ANON_VOTE=true - VOTES_IP=true - AUTH_EMAIL_ENABLE=true - AUTH_ANON=true