Reject reservation limits in endpoint

This commit is contained in:
binwiederhier 2023-01-05 21:15:10 -05:00
parent 1bc40693bb
commit a51d95743a
4 changed files with 47 additions and 9 deletions

View file

@ -69,8 +69,9 @@ var (
errHTTPTooManyRequestsLimitEmails = &errHTTP{42902, http.StatusTooManyRequests, "limit reached: too many emails, please be nice", "https://ntfy.sh/docs/publish/#limitations"} errHTTPTooManyRequestsLimitEmails = &errHTTP{42902, http.StatusTooManyRequests, "limit reached: too many emails, please be nice", "https://ntfy.sh/docs/publish/#limitations"}
errHTTPTooManyRequestsLimitSubscriptions = &errHTTP{42903, http.StatusTooManyRequests, "limit reached: too many active subscriptions, please be nice", "https://ntfy.sh/docs/publish/#limitations"} errHTTPTooManyRequestsLimitSubscriptions = &errHTTP{42903, http.StatusTooManyRequests, "limit reached: too many active subscriptions, please be nice", "https://ntfy.sh/docs/publish/#limitations"}
errHTTPTooManyRequestsLimitTotalTopics = &errHTTP{42904, http.StatusTooManyRequests, "limit reached: the total number of topics on the server has been reached, please contact the admin", "https://ntfy.sh/docs/publish/#limitations"} errHTTPTooManyRequestsLimitTotalTopics = &errHTTP{42904, http.StatusTooManyRequests, "limit reached: the total number of topics on the server has been reached, please contact the admin", "https://ntfy.sh/docs/publish/#limitations"}
errHTTPTooManyRequestsAttachmentBandwidthLimit = &errHTTP{42905, http.StatusTooManyRequests, "too many requests: daily bandwidth limit reached", "https://ntfy.sh/docs/publish/#limitations"} errHTTPTooManyRequestsLimitAttachmentBandwidth = &errHTTP{42905, http.StatusTooManyRequests, "limit reached: daily bandwidth", "https://ntfy.sh/docs/publish/#limitations"}
errHTTPTooManyRequestsAccountCreateLimit = &errHTTP{42906, http.StatusTooManyRequests, "too many requests: daily account creation limit reached", "https://ntfy.sh/docs/publish/#limitations"} // FIXME document limit errHTTPTooManyRequestsLimitAccountCreation = &errHTTP{42906, http.StatusTooManyRequests, "limit reached: too many accounts created", "https://ntfy.sh/docs/publish/#limitations"} // FIXME document limit
errHTTPTooManyRequestsLimitReservations = &errHTTP{42907, http.StatusTooManyRequests, "limit reached: too many topic reservations for this user", ""}
errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", ""} errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", ""}
errHTTPInternalErrorInvalidPath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid path", ""} errHTTPInternalErrorInvalidPath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid path", ""}
errHTTPInternalErrorMissingBaseURL = &errHTTP{50003, http.StatusInternalServerError, "internal server error: base-url must be be configured for this feature", "https://ntfy.sh/docs/config/"} errHTTPInternalErrorMissingBaseURL = &errHTTP{50003, http.StatusInternalServerError, "internal server error: base-url must be be configured for this feature", "https://ntfy.sh/docs/config/"}

View file

@ -36,26 +36,31 @@ import (
/* /*
TODO TODO
limits: limits & rate limiting:
message cache duration message cache duration
Keep 10000 messages or keep X days? Keep 10000 messages or keep X days?
Attachment expiration based on plan Attachment expiration based on plan
login/account endpoints
plan: plan:
weirdness with admin and "default" account weirdness with admin and "default" account
"account topic" sync mechanism
v.Info() endpoint double selects from DB v.Info() endpoint double selects from DB
JS constants
purge accounts that were not logged into in X purge accounts that were not logged into in X
reset daily limits for users reset daily limits for users
Make sure account endpoints make sense for admins
UI: UI:
- flicker of upgrade banner - flicker of upgrade banner
- JS constants
- useContext for account
Sync: Sync:
- "account topic" sync mechanism
- "mute" setting - "mute" setting
- figure out what settings are "web" or "phone" - figure out what settings are "web" or "phone"
rate limiting:
- login/account endpoints
Tests: Tests:
- /access endpoints
- visitor with/without user - visitor with/without user
Refactor:
- rename TopicsLimit -> ReservationsLimit
- rename /access -> /reservation
Later: Later:
- Password reset - Password reset
- Pricing - Pricing
@ -496,7 +501,7 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, v *visitor)
} }
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
if err := v.BandwidthLimiter().Allow(stat.Size()); err != nil { if err := v.BandwidthLimiter().Allow(stat.Size()); err != nil {
return errHTTPTooManyRequestsAttachmentBandwidthLimit return errHTTPTooManyRequestsLimitAttachmentBandwidth
} }
} }
w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size())) w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size()))

View file

@ -2,6 +2,7 @@ package server
import ( import (
"encoding/json" "encoding/json"
"errors"
"heckel.io/ntfy/user" "heckel.io/ntfy/user"
"heckel.io/ntfy/util" "heckel.io/ntfy/util"
"net/http" "net/http"
@ -29,7 +30,7 @@ func (s *Server) handleAccountCreate(w http.ResponseWriter, r *http.Request, v *
return errHTTPConflictUserExists return errHTTPConflictUserExists
} }
if v.accountLimiter != nil && !v.accountLimiter.Allow() { if v.accountLimiter != nil && !v.accountLimiter.Allow() {
return errHTTPTooManyRequestsAccountCreateLimit return errHTTPTooManyRequestsLimitAccountCreation
} }
if err := s.userManager.AddUser(newAccount.Username, newAccount.Password, user.RoleUser); err != nil { // TODO this should return a User if err := s.userManager.AddUser(newAccount.Username, newAccount.Password, user.RoleUser); err != nil { // TODO this should return a User
return err return err
@ -331,6 +332,15 @@ func (s *Server) handleAccountAccessAdd(w http.ResponseWriter, r *http.Request,
if !topicRegex.MatchString(req.Topic) { if !topicRegex.MatchString(req.Topic) {
return errHTTPBadRequestTopicInvalid return errHTTPBadRequestTopicInvalid
} }
if v.user.Plan == nil {
return errors.New("no plan") // FIXME there should always be a plan!
}
reservations, err := s.userManager.ReservationsCount(v.user.Name)
if err != nil {
return err
} else if reservations >= v.user.Plan.TopicsLimit {
return errHTTPTooManyRequestsLimitReservations // FIXME test this
}
if err := s.userManager.CheckAllowAccess(v.user.Name, req.Topic); err != nil { if err := s.userManager.CheckAllowAccess(v.user.Name, req.Topic); err != nil {
return errHTTPConflictTopicReserved return errHTTPConflictTopicReserved
} }

View file

@ -140,6 +140,11 @@ const (
AND a_user.owner_user_id = (SELECT id FROM user WHERE user = ?) AND a_user.owner_user_id = (SELECT id FROM user WHERE user = ?)
ORDER BY a_user.topic ORDER BY a_user.topic
` `
selectUserReservationsCountQuery = `
SELECT COUNT(*)
FROM user_access
WHERE user_id = owner_user_id AND owner_user_id = (SELECT id FROM user WHERE user = ?)
`
selectOtherAccessCountQuery = ` selectOtherAccessCountQuery = `
SELECT COUNT(*) SELECT COUNT(*)
FROM user_access FROM user_access
@ -599,6 +604,23 @@ func (a *Manager) Reservations(username string) ([]Reservation, error) {
return reservations, nil return reservations, nil
} }
// ReservationsCount returns the number of reservations owned by this user
func (a *Manager) ReservationsCount(username string) (int64, error) {
rows, err := a.db.Query(selectUserReservationsCountQuery, username)
if err != nil {
return 0, err
}
defer rows.Close()
if !rows.Next() {
return 0, errNoRows
}
var count int64
if err := rows.Scan(&count); err != nil {
return 0, err
}
return count, nil
}
// ChangePassword changes a user's password // ChangePassword changes a user's password
func (a *Manager) ChangePassword(username, password string) error { func (a *Manager) ChangePassword(username, password string) error {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost) hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)