~evanj/cms

10de0b32f424dd6ae362ed23a39b9e40dfc3cfa9 — Evan M Jones 1 year, 11 days ago 7092668
Fix(rate limiting): Refactor file upload access to RL.
M cms.go => cms.go +1 -1
@@ 106,8 106,8 @@ func init() {
		applogger.Fatal(err)
	}

	rl := rl.New(log.New(w, "[cms:ratelimit] ", 0), cacher)
	fs := e3.New(e3user, e3pass, e3url)
	rl := rl.New(log.New(w, "[cms:ratelimit] ", 0), cacher, fs)
	c := c.New(log.New(w, "[cms:content] ", 0), rl, analyticsEnabled)
	libs := libstripe.New(stripeSuccessURL, stripeErrorURL, stripePK, stripeSK, rl)


M internal/c/c.go => internal/c/c.go +1 -4
@@ 21,8 21,7 @@ type KeyCookie = string
var (
	KeyUserLogin KeyCookie = "KeyUserLogin"

	ErrNoLogin  = errors.New("must be logged in")
	ErrNoAccess = errors.New("you don't have access to that feature")
	ErrNoLogin = errors.New("must be logged in")
)

type Controller struct {


@@ 147,7 146,6 @@ func (c *Controller) HTML(w http.ResponseWriter, r *http.Request, tmpl *template
	}

	if err := tmpl.Execute(&buf, data); err != nil {
		c.log.Println(err)
		c.Error(w, r, http.StatusInternalServerError, "failed to build html response")
		return
	}


@@ 164,7 162,6 @@ func (c *Controller) HTML(w http.ResponseWriter, r *http.Request, tmpl *template
func (c *Controller) JSON(w http.ResponseWriter, r *http.Request, data interface{}) {
	bytes, err := json.Marshal(data)
	if err != nil {
		c.log.Println(err)
		c.Error(w, r, http.StatusInternalServerError, "failed to build json response")
		return
	}

M internal/c/content/content.go => internal/c/content/content.go +4 -10
@@ 15,7 15,6 @@ import (
	"git.sr.ht/~evanj/cms/internal/m/content"
	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/space"
	"git.sr.ht/~evanj/cms/internal/m/tier"
	"git.sr.ht/~evanj/cms/internal/m/user"
	"git.sr.ht/~evanj/cms/internal/m/valuetype"
	"git.sr.ht/~evanj/cms/internal/s/db"


@@ 26,11 25,10 @@ import (
var (
	contentHTML = v.MustParse("html/content.html")

	ErrNoLogin  = c.ErrNoLogin
	ErrNoAccess = c.ErrNoAccess
	ErrNoSpace  = errors.New("failed to find required space")
	ErrNoCT     = errors.New("failed to find required contenttype")
	ErrNoC      = errors.New("failed to find desired content")
	ErrNoLogin = c.ErrNoLogin
	ErrNoSpace = errors.New("failed to find required space")
	ErrNoCT    = errors.New("failed to find required contenttype")
	ErrNoC     = errors.New("failed to find desired content")
)

type Content struct {


@@ 74,10 72,6 @@ func New(c *c.Controller, log *log.Logger, db DBer, e3 E3er, hook Hooker, baseUR
}

func (c *Content) upload(ctx context.Context, filename string, file io.Reader, u user.User) (string, error) {
	if u.Org().Tier().Is(tier.Free) {
		return "", fmt.Errorf("can't upload file: %w", ErrNoAccess)
	}

	raw, err := c.e3.Upload(ctx, false, filename, file)
	if err != nil {
		return "", err

M internal/c/contenttype/contenttype.go => internal/c/contenttype/contenttype.go +4 -21
@@ 12,9 12,7 @@ import (
	"git.sr.ht/~evanj/cms/internal/m/content"
	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/space"
	"git.sr.ht/~evanj/cms/internal/m/tier"
	"git.sr.ht/~evanj/cms/internal/m/user"
	"git.sr.ht/~evanj/cms/internal/m/valuetype"
	"git.sr.ht/~evanj/cms/internal/s/db"
	"git.sr.ht/~evanj/cms/internal/v"
)


@@ 75,7 73,7 @@ func (c *ContentType) tree(w http.ResponseWriter, r *http.Request) (user.User, s
func (ct *ContentType) create(w http.ResponseWriter, r *http.Request) {
	name := r.FormValue("name")

	user, space, err := ct.tree(w, r)
	_, space, err := ct.tree(w, r)
	if err != nil {
		ct.Error2(w, r, http.StatusBadRequest, err)
		return


@@ 100,11 98,6 @@ func (ct *ContentType) create(w http.ResponseWriter, r *http.Request) {
			return
		}

		if valType == valuetype.File && user.Org().Tier().Is(tier.Free) {
			ct.Error2(w, r, http.StatusBadRequest, fmt.Errorf("can't create value type of file: %w", c.ErrNoAccess))
			return
		}

		params = append(params, db.ContentTypeNewParam{
			Name: valName,
			Type: valType,


@@ 130,7 123,7 @@ func (ct *ContentType) create(w http.ResponseWriter, r *http.Request) {

	ctype, err := ct.db.ContentTypeNew(space, name, params)
	if err != nil {
		ct.Error2(w, r, http.StatusInternalServerError, ErrFailedCreate)
		ct.Error2(w, r, http.StatusInternalServerError, fmt.Errorf("%s: %w", ErrFailedCreate.Error(), err))
		return
	}



@@ 142,7 135,7 @@ func (ct *ContentType) update(w http.ResponseWriter, r *http.Request) {
	name := r.FormValue("name")
	ctID := r.FormValue("contenttype")

	user, space, err := ct.tree(w, r)
	_, space, err := ct.tree(w, r)
	if err != nil {
		ct.Error2(w, r, http.StatusBadRequest, err)
		return


@@ 175,11 168,6 @@ func (ct *ContentType) update(w http.ResponseWriter, r *http.Request) {
			return
		}

		if valType == valuetype.File && user.Org().Tier().Is(tier.Free) {
			ct.Error2(w, r, http.StatusBadRequest, fmt.Errorf("can't create value type of file: %w", c.ErrNoAccess))
			return
		}

		newParams = append(newParams, db.ContentTypeNewParam{
			Name: valName,
			Type: valType,


@@ 206,11 194,6 @@ func (ct *ContentType) update(w http.ResponseWriter, r *http.Request) {
			return
		}

		if valType == valuetype.File && user.Org().Tier().Is(tier.Free) {
			ct.Error2(w, r, http.StatusBadRequest, fmt.Errorf("can't create value type of file: %w", c.ErrNoAccess))
			return
		}

		updateParams = append(updateParams, db.ContentTypeUpdateParam{
			ID:   valID,
			Name: valName,


@@ 248,7 231,7 @@ func (ct *ContentType) update(w http.ResponseWriter, r *http.Request) {

	ctype, err = ct.db.ContentTypeUpdate(space, ctype, name, newParams, updateParams)
	if err != nil {
		ct.Error2(w, r, http.StatusInternalServerError, ErrFailedUpdate)
		ct.Error2(w, r, http.StatusInternalServerError, fmt.Errorf("%s: %w", ErrFailedUpdate.Error(), err))
		return
	}


M internal/c/space/space.go => internal/c/space/space.go +0 -4
@@ 78,7 78,6 @@ func (s *Space) serve(w http.ResponseWriter, r *http.Request) {
	beforehook, _ := strconv.Atoi(r.URL.Query().Get("beforehook"))
	hooks, err := s.db.HooksPerSpace(space, beforehook)
	if err != nil {
		s.log.Println(err)
		s.Error(w, r, http.StatusInternalServerError, "failed to find webhooks for space")
		return
	}


@@ 131,7 130,6 @@ func (s *Space) copy(w http.ResponseWriter, r *http.Request) {

	spaceNext, err := s.db.SpaceCopy(user, spacePrev, name, desc)
	if err != nil {
		s.log.Println(err)
		s.Error(w, r, http.StatusBadRequest, err.Error())
		return
	}


@@ 160,7 158,6 @@ func (s *Space) update(w http.ResponseWriter, r *http.Request) {

	next, err := s.db.SpaceUpdate(user, prev, name, desc)
	if err != nil {
		s.log.Println(err)
		s.Error(w, r, http.StatusInternalServerError, "failed to update space")
		return
	}


@@ 186,7 183,6 @@ func (s *Space) delete(w http.ResponseWriter, r *http.Request) {
	}

	if err := s.db.SpaceDelete(user, space); err != nil {
		s.log.Println(err)
		s.Error(w, r, http.StatusInternalServerError, "failed to delete space")
		return
	}

M internal/c/user/user.go => internal/c/user/user.go +0 -2
@@ 2,7 2,6 @@ package user

import (
	"errors"
	"fmt"
	"log"
	"net/http"
	"net/url"


@@ 80,7 79,6 @@ func (l *User) signup(w http.ResponseWriter, r *http.Request) {
	verify := r.FormValue("verify")
	t, ok := tier.ByName(r.FormValue("tier"))
	if !ok {
		fmt.Println(r.FormValue("tier"))
		l.Error2(w, r, http.StatusBadRequest, ErrNoTier)
		return
	}

M internal/m/space/space.go => internal/m/space/space.go +3 -0
@@ 1,9 1,12 @@
package space

import "git.sr.ht/~evanj/cms/internal/m/org"

type Space interface {
	ID() string
	Name() string
	Desc() string
	Org() org.Org
}

type SpaceList interface {

M internal/m/tier/tier.go => internal/m/tier/tier.go +2 -2
@@ 79,7 79,7 @@ func ByName(n string) (Tier, bool) {
			return t, true
		}
	}
	return Tier{}, false
	return Free, false
}

func ByStripePriceID(n string) (Tier, bool) {


@@ 88,7 88,7 @@ func ByStripePriceID(n string) (Tier, bool) {
			return t, true
		}
	}
	return Tier{}, false
	return Free, false
}

func (t Tier) Is(test Tier) bool {

M internal/s/db/content.go => internal/s/db/content.go +0 -1
@@ 643,7 643,6 @@ func (db *DB) contentUpdate(t *sql.Tx, space space.Space, ct contenttype.Content
		}

		if _, err := t.Exec(queryValueUpdate, item.Value, item.ID); err != nil {
			db.log.Println(err)
			return fmt.Errorf("failed to create update content value '%s'", item.Value)
		}
	}

M internal/s/db/contenttype.go => internal/s/db/contenttype.go +0 -4
@@ 218,7 218,6 @@ func (db *DB) ContentTypesPerSpace(space space.Space, before int) (contenttype.C

	list, err := db.contentTypesPerSpace(t, space, before)
	if err != nil {
		db.log.Println(err)
		return nil, err
	}



@@ 277,19 276,16 @@ func (db *DB) ContentTypeSearch(space space.Space, query string, before int) (co

	rows, err := db.Query(q, fmt.Sprintf("%%%s%%", query), space.ID(), before, perPage)
	if err != nil {
		db.log.Println("1", err)
		return nil, err
	}

	for rows.Next() {
		if err := rows.Scan(&id); err != nil {
			db.log.Println("2", err)
			return nil, err
		}

		ct, err := db.ContentTypeGet(space, strconv.Itoa(id))
		if err != nil {
			db.log.Println("3", err)
			return nil, err
		}


M internal/s/db/db.go => internal/s/db/db.go +0 -1
@@ 224,7 224,6 @@ func (db *DB) FileExists(URL string) (bool, error) {

	var spaceID string
	if err := db.QueryRow(q, valuetype.File, URL).Scan(&spaceID); err != nil {
		db.log.Println("FileExists", err)
		return false, err
	}


M internal/s/db/hook.go => internal/s/db/hook.go +0 -1
@@ 143,7 143,6 @@ func (db *DB) HooksPerSpace(space space.Space, before int) (hook.HookList, error

	list, err := db.hooksPerSpace(t, space, before)
	if err != nil {
		db.log.Println(err)
		return nil, err
	}


M internal/s/db/space.go => internal/s/db/space.go +22 -18
@@ 9,7 9,9 @@ import (

	"git.sr.ht/~evanj/cms/internal/m/content"
	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/org"
	"git.sr.ht/~evanj/cms/internal/m/space"
	"git.sr.ht/~evanj/cms/internal/m/tier"
	"git.sr.ht/~evanj/cms/internal/m/user"
	"git.sr.ht/~evanj/cms/internal/m/valuetype"
)


@@ 18,6 20,13 @@ type Space struct {
	SpaceID   string
	SpaceName string
	SpaceDesc string
	// Set on fetch.
	SpaceOrg spaceOrg
}

type spaceOrg struct {
	OrgID       string
	OrgTierName string
}

var (


@@ 73,12 82,7 @@ func (db *DB) spaceNew(t *sql.Tx, user user.User, name, desc string) (space.Spac
		return nil, fmt.Errorf("failed to create space")
	}

	var space Space
	if err := t.QueryRow(queryFindSpaceByUserAndID, user.ID(), id).Scan(&space.SpaceID, &space.SpaceName, &space.SpaceDesc); err != nil {
		return nil, fmt.Errorf("failed to find space created")
	}

	return &space, nil
	return db.spaceGet(t, user, strconv.FormatInt(id, 10))
}

func (db *DB) SpaceNew(user user.User, name, desc string) (space.Space, error) {


@@ 343,7 347,12 @@ func (db *DB) spaceUpdateContent(t *sql.Tx, next space.Space, ct contenttype.Con
}

func (db *DB) spaceGet(t *sql.Tx, user user.User, spaceID string) (space.Space, error) {
	var space Space
	space := Space{
		SpaceOrg: spaceOrg{
			OrgID:       user.Org().ID(),
			OrgTierName: user.Org().Tier().Name,
		},
	}
	err := t.QueryRow(queryFindSpaceByUserAndID, user.ID(), spaceID).Scan(&space.SpaceID, &space.SpaceName, &space.SpaceDesc)
	if err != nil {
		return nil, fmt.Errorf("failed to find space")


@@ 432,24 441,19 @@ func (db *DB) SpacesPerUser(user user.User, before int) (space.SpaceList, error)

	list, err := db.spacesPerUser(t, user, before)
	if err != nil {
		db.log.Println(err)
		return nil, err
	}

	return list, t.Commit()
}

func (s *Space) ID() string {
	return s.SpaceID
}
func (s *Space) ID() string   { return s.SpaceID }
func (s *Space) Name() string { return s.SpaceName }
func (s *Space) Desc() string { return s.SpaceDesc }
func (s *Space) Org() org.Org { return s.SpaceOrg }

func (s *Space) Name() string {
	return s.SpaceName
}

func (s *Space) Desc() string {
	return s.SpaceDesc
}
func (so spaceOrg) ID() string      { return so.OrgID }
func (so spaceOrg) Tier() tier.Tier { t, _ := tier.ByName(so.OrgTierName); return t } // Guaranteed to always be set correctly.

// SPACE LIST STRUCT / INTERFACE


M internal/s/db/user.go => internal/s/db/user.go +0 -7
@@ 50,7 50,6 @@ func (db *DB) userNew(t *sql.Tx, username, password, verifyPassword string) (use

	hash, err := db.sec.HashCreate(username, password)
	if err != nil {
		db.log.Println(err)
		return nil, fmt.Errorf("failed to create password hash")
	}



@@ 87,7 86,6 @@ func (db *DB) userNew(t *sql.Tx, username, password, verifyPassword string) (use
func (db *DB) UserGet(username, password string) (user.User, error) {
	var user User
	if err := db.QueryRow(queryFindUserByName, username).Scan(&user.UserID, &user.UserName, &user.userHash, &user.userOrgID); err != nil {
		db.log.Println(err)
		return nil, fmt.Errorf("failed to find user '%s'", username)
	}



@@ 97,7 95,6 @@ func (db *DB) UserGet(username, password string) (user.User, error) {

	tok, err := db.sec.TokenCreate(security.TokenMap{"ID": user.UserID})
	if err != nil {
		db.log.Println(err)
		return nil, fmt.Errorf("failed to create token for user")
	}



@@ 115,25 112,21 @@ func (db *DB) UserGet(username, password string) (user.User, error) {
func (db *DB) UserGetFromToken(token string) (user.User, error) {
	tmap, err := db.sec.TokenFrom(token)
	if err != nil {
		db.log.Println(err)
		return nil, fmt.Errorf("failed to decode user token")
	}

	id, ok := tmap["ID"]
	if !ok {
		db.log.Println(err)
		return nil, fmt.Errorf("corrupted user token")
	}

	var user User
	if err := db.QueryRow(queryFindUserByID, id).Scan(&user.UserID, &user.UserName, &user.userHash, &user.userOrgID); err != nil {
		db.log.Println(err)
		return nil, fmt.Errorf("failed to find user")
	}

	tok, err := db.sec.TokenCreate(security.TokenMap{"ID": user.UserID})
	if err != nil {
		db.log.Println(err)
		return nil, fmt.Errorf("failed to create token for user")
	}


M internal/s/rl/rl.go => internal/s/rl/rl.go +53 -2
@@ 2,16 2,22 @@
package rl

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log"
	"time"

	"git.sr.ht/~evanj/cms/internal/m/contenttype"
	"git.sr.ht/~evanj/cms/internal/m/org"
	"git.sr.ht/~evanj/cms/internal/m/space"
	"git.sr.ht/~evanj/cms/internal/m/tier"
	"git.sr.ht/~evanj/cms/internal/m/user"
	"git.sr.ht/~evanj/cms/internal/m/valuetype"
	"git.sr.ht/~evanj/cms/internal/s/cache"
	"git.sr.ht/~evanj/cms/internal/s/db"
	"git.sr.ht/~evanj/cms/pkg/e3"
)

var (


@@ 28,17 34,20 @@ var (
	}

	ErrHitLimit = errors.New("you have surpassed your usage limit: consider upgrading")
	ErrNoAccess = errors.New("you don't have access to that feature")
)

type RL struct {
	*cache.Cache
	e3.E3

	log *log.Logger
	db  *cache.Cache // Or *db.DB depending on nest order.
	e3  e3.E3
}

func New(l *log.Logger, db *cache.Cache) RL {
	return RL{db, l, db}
func New(l *log.Logger, db *cache.Cache, e3 e3.E3) RL {
	return RL{db, e3, l, db, e3}
}

// Limit requests made.


@@ 115,3 124,45 @@ func (rl RL) SpaceCopy(user user.User, prevS space.Space, name, desc string) (sp

// TODO: Limit users created (and associated with org). Currently, no support
// for adding more users to an org.

// Limit file uploads.

func (rl RL) Upload(ctx context.Context, public bool, filename string, file io.Reader, u user.User) (string, error) {
	if u.Org().Tier().Is(tier.Free) {
		return "", fmt.Errorf("can't upload file: %w", ErrNoAccess)
	}

	return rl.e3.Upload(ctx, public, filename, file)
}

func newParamHasFile(params []db.ContentTypeNewParam) (r bool) {
	for _, p := range params {
		if p.Type == valuetype.File {
			r = true
		}
	}
	return
}

func updateParamHasFile(params []db.ContentTypeUpdateParam) (r bool) {
	for _, p := range params {
		if p.Type == valuetype.File {
			r = true
		}
	}
	return
}

func (rl RL) ContentTypeNew(space space.Space, name string, params []db.ContentTypeNewParam) (contenttype.ContentType, error) {
	if space.Org().Tier().Is(tier.Free) && newParamHasFile(params) {
		return nil, fmt.Errorf("can't create content type with field type of file: %w", ErrNoAccess)
	}
	return rl.db.ContentTypeNew(space, name, params)
}

func (rl RL) ContentTypeUpdate(space space.Space, contenttype contenttype.ContentType, name string, newParams []db.ContentTypeNewParam, updateParams []db.ContentTypeUpdateParam) (contenttype.ContentType, error) {
	if space.Org().Tier().Is(tier.Free) && (newParamHasFile(newParams) || updateParamHasFile(updateParams)) {
		return nil, fmt.Errorf("can't create content type with field type of file: %w", ErrNoAccess)
	}
	return rl.db.ContentTypeUpdate(space, contenttype, name, newParams, updateParams)
}