Allow completely private instances, part 1

This is the start of all changes needed to support entirely private
instances, where all blogs are only visible to other authenticated users
on an instance (ref T576). It begins by changing how Handler methods check an
endpoint's permissions.

- Renames UserLevelLEVEL consts to UserLevelLEVELType
- Adds UserLevelLEVEL funcs with same names as previous consts. Each
  returns a UserLevel
- Adds a new UserLevelReader that restricts access based on app
  configuration. This is now used on collections and posts.
- Changes routing a bit so static files are always accessible
This commit is contained in:
Matt Baer 2019-06-16 18:55:50 -04:00
parent 161f7a8de2
commit b3a36a3be7
4 changed files with 87 additions and 38 deletions

View File

@ -76,7 +76,9 @@ type (
// Federation // Federation
Federation bool `ini:"federation"` Federation bool `ini:"federation"`
PublicStats bool `ini:"public_stats"` PublicStats bool `ini:"public_stats"`
Private bool `ini:"private"`
// Access
Private bool `ini:"private"`
// Additional functions // Additional functions
LocalTimeline bool `ini:"local_timeline"` LocalTimeline bool `ini:"local_timeline"`

View File

@ -23,24 +23,52 @@ import (
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/writeas/impart" "github.com/writeas/impart"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writeas/writefreely/config"
"github.com/writeas/writefreely/page" "github.com/writeas/writefreely/page"
) )
// UserLevel represents the required user level for accessing an endpoint
type UserLevel int type UserLevel int
const ( const (
UserLevelNone UserLevel = iota // user or not -- ignored UserLevelNoneType UserLevel = iota // user or not -- ignored
UserLevelOptional // user or not -- object fetched if user UserLevelOptionalType // user or not -- object fetched if user
UserLevelNoneRequired // non-user (required) UserLevelNoneRequiredType // non-user (required)
UserLevelUser // user (required) UserLevelUserType // user (required)
) )
func UserLevelNone(cfg *config.Config) UserLevel {
return UserLevelNoneType
}
func UserLevelOptional(cfg *config.Config) UserLevel {
return UserLevelOptionalType
}
func UserLevelNoneRequired(cfg *config.Config) UserLevel {
return UserLevelNoneRequiredType
}
func UserLevelUser(cfg *config.Config) UserLevel {
return UserLevelUserType
}
// UserLevelReader returns the permission level required for any route where
// users can read published content.
func UserLevelReader(cfg *config.Config) UserLevel {
if cfg.App.Private {
return UserLevelUserType
}
return UserLevelOptionalType
}
type ( type (
handlerFunc func(app *App, w http.ResponseWriter, r *http.Request) error handlerFunc func(app *App, w http.ResponseWriter, r *http.Request) error
userHandlerFunc func(app *App, u *User, w http.ResponseWriter, r *http.Request) error userHandlerFunc func(app *App, u *User, w http.ResponseWriter, r *http.Request) error
userApperHandlerFunc func(apper Apper, u *User, w http.ResponseWriter, r *http.Request) error userApperHandlerFunc func(apper Apper, u *User, w http.ResponseWriter, r *http.Request) error
dataHandlerFunc func(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) dataHandlerFunc func(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error)
authFunc func(app *App, r *http.Request) (*User, error) authFunc func(app *App, r *http.Request) (*User, error)
UserLevelFunc func(cfg *config.Config) UserLevel
) )
type Handler struct { type Handler struct {
@ -307,7 +335,7 @@ func (h *Handler) Page(n string) http.HandlerFunc {
}, UserLevelOptional) }, UserLevelOptional)
} }
func (h *Handler) WebErrors(f handlerFunc, ul UserLevel) http.HandlerFunc { func (h *Handler) WebErrors(f handlerFunc, ul UserLevelFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
// TODO: factor out this logic shared with Web() // TODO: factor out this logic shared with Web()
h.handleHTTPError(w, r, func() error { h.handleHTTPError(w, r, func() error {
@ -331,21 +359,21 @@ func (h *Handler) WebErrors(f handlerFunc, ul UserLevel) http.HandlerFunc {
var session *sessions.Session var session *sessions.Session
var err error var err error
if ul != UserLevelNone { if ul(h.app.App().cfg) != UserLevelNoneType {
session, err = h.sessionStore.Get(r, cookieName) session, err = h.sessionStore.Get(r, cookieName)
if err != nil && (ul == UserLevelNoneRequired || ul == UserLevelUser) { if err != nil && (ul(h.app.App().cfg) == UserLevelNoneRequiredType || ul(h.app.App().cfg) == UserLevelUserType) {
// Cookie is required, but we can ignore this error // Cookie is required, but we can ignore this error
log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul, err) log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul, err)
} }
_, gotUser := session.Values[cookieUserVal].(*User) _, gotUser := session.Values[cookieUserVal].(*User)
if ul == UserLevelNoneRequired && gotUser { if ul(h.app.App().cfg) == UserLevelNoneRequiredType && gotUser {
to := correctPageFromLoginAttempt(r) to := correctPageFromLoginAttempt(r)
log.Info("Handler: Required NO user, but got one. Redirecting to %s", to) log.Info("Handler: Required NO user, but got one. Redirecting to %s", to)
err := impart.HTTPError{http.StatusFound, to} err := impart.HTTPError{http.StatusFound, to}
status = err.Status status = err.Status
return err return err
} else if ul == UserLevelUser && !gotUser { } else if ul(h.app.App().cfg) == UserLevelUserType && !gotUser {
log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.") log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.")
err := ErrNotLoggedIn err := ErrNotLoggedIn
status = err.Status status = err.Status
@ -380,9 +408,18 @@ func (h *Handler) WebErrors(f handlerFunc, ul UserLevel) http.HandlerFunc {
} }
} }
func (h *Handler) CollectionPostOrStatic(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, ".") && !isRaw(r) {
// Serve static file
h.app.App().shttp.ServeHTTP(w, r)
}
h.Web(viewCollectionPost, UserLevelReader)(w, r)
}
// Web handles requests made in the web application. This provides user- // Web handles requests made in the web application. This provides user-
// friendly HTML pages and actions that work in the browser. // friendly HTML pages and actions that work in the browser.
func (h *Handler) Web(f handlerFunc, ul UserLevel) http.HandlerFunc { func (h *Handler) Web(f handlerFunc, ul UserLevelFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
h.handleHTTPError(w, r, func() error { h.handleHTTPError(w, r, func() error {
var status int var status int
@ -404,21 +441,21 @@ func (h *Handler) Web(f handlerFunc, ul UserLevel) http.HandlerFunc {
log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent()) log.Info("\"%s %s\" %d %s \"%s\"", r.Method, r.RequestURI, status, time.Since(start), r.UserAgent())
}() }()
if ul != UserLevelNone { if ul(h.app.App().cfg) != UserLevelNoneType {
session, err := h.sessionStore.Get(r, cookieName) session, err := h.sessionStore.Get(r, cookieName)
if err != nil && (ul == UserLevelNoneRequired || ul == UserLevelUser) { if err != nil && (ul(h.app.App().cfg) == UserLevelNoneRequiredType || ul(h.app.App().cfg) == UserLevelUserType) {
// Cookie is required, but we can ignore this error // Cookie is required, but we can ignore this error
log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul, err) log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul, err)
} }
_, gotUser := session.Values[cookieUserVal].(*User) _, gotUser := session.Values[cookieUserVal].(*User)
if ul == UserLevelNoneRequired && gotUser { if ul(h.app.App().cfg) == UserLevelNoneRequiredType && gotUser {
to := correctPageFromLoginAttempt(r) to := correctPageFromLoginAttempt(r)
log.Info("Handler: Required NO user, but got one. Redirecting to %s", to) log.Info("Handler: Required NO user, but got one. Redirecting to %s", to)
err := impart.HTTPError{http.StatusFound, to} err := impart.HTTPError{http.StatusFound, to}
status = err.Status status = err.Status
return err return err
} else if ul == UserLevelUser && !gotUser { } else if ul(h.app.App().cfg) == UserLevelUserType && !gotUser {
log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.") log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.")
err := ErrNotLoggedIn err := ErrNotLoggedIn
status = err.Status status = err.Status
@ -478,7 +515,7 @@ func (h *Handler) All(f handlerFunc) http.HandlerFunc {
} }
} }
func (h *Handler) Download(f dataHandlerFunc, ul UserLevel) http.HandlerFunc { func (h *Handler) Download(f dataHandlerFunc, ul UserLevelFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
h.handleHTTPError(w, r, func() error { h.handleHTTPError(w, r, func() error {
var status int var status int
@ -523,27 +560,27 @@ func (h *Handler) Download(f dataHandlerFunc, ul UserLevel) http.HandlerFunc {
} }
} }
func (h *Handler) Redirect(url string, ul UserLevel) http.HandlerFunc { func (h *Handler) Redirect(url string, ul UserLevelFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
h.handleHTTPError(w, r, func() error { h.handleHTTPError(w, r, func() error {
start := time.Now() start := time.Now()
var status int var status int
if ul != UserLevelNone { if ul(h.app.App().cfg) != UserLevelNoneType {
session, err := h.sessionStore.Get(r, cookieName) session, err := h.sessionStore.Get(r, cookieName)
if err != nil && (ul == UserLevelNoneRequired || ul == UserLevelUser) { if err != nil && (ul(h.app.App().cfg) == UserLevelNoneRequiredType || ul(h.app.App().cfg) == UserLevelUserType) {
// Cookie is required, but we can ignore this error // Cookie is required, but we can ignore this error
log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul, err) log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul, err)
} }
_, gotUser := session.Values[cookieUserVal].(*User) _, gotUser := session.Values[cookieUserVal].(*User)
if ul == UserLevelNoneRequired && gotUser { if ul(h.app.App().cfg) == UserLevelNoneRequiredType && gotUser {
to := correctPageFromLoginAttempt(r) to := correctPageFromLoginAttempt(r)
log.Info("Handler: Required NO user, but got one. Redirecting to %s", to) log.Info("Handler: Required NO user, but got one. Redirecting to %s", to)
err := impart.HTTPError{http.StatusFound, to} err := impart.HTTPError{http.StatusFound, to}
status = err.Status status = err.Status
return err return err
} else if ul == UserLevelUser && !gotUser { } else if ul(h.app.App().cfg) == UserLevelUserType && !gotUser {
log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.") log.Info("Handler: Required a user, but DIDN'T get one. Sending not logged in.")
err := ErrNotLoggedIn err := ErrNotLoggedIn
status = err.Status status = err.Status

View File

@ -1190,6 +1190,16 @@ func getRawCollectionPost(app *App, slug, collAlias string) *RawPost {
} }
} }
func isRaw(r *http.Request) bool {
vars := mux.Vars(r)
slug := vars["slug"]
isJSON := strings.HasSuffix(slug, ".json")
isXML := strings.HasSuffix(slug, ".xml")
isMarkdown := strings.HasSuffix(slug, ".md")
return strings.HasSuffix(slug, ".txt") || isJSON || isXML || isMarkdown
}
func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error { func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r) vars := mux.Vars(r)
slug := vars["slug"] slug := vars["slug"]
@ -1199,12 +1209,6 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error
isMarkdown := strings.HasSuffix(slug, ".md") isMarkdown := strings.HasSuffix(slug, ".md")
isRaw := strings.HasSuffix(slug, ".txt") || isJSON || isXML || isMarkdown isRaw := strings.HasSuffix(slug, ".txt") || isJSON || isXML || isMarkdown
if strings.Contains(r.URL.Path, ".") && !isRaw {
// Serve static file
app.shttp.ServeHTTP(w, r)
return nil
}
cr := &collectionReq{} cr := &collectionReq{}
err := processCollectionRequest(cr, vars, w, r) err := processCollectionRequest(cr, vars, w, r)
if err != nil { if err != nil {

View File

@ -14,6 +14,7 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/writeas/go-webfinger" "github.com/writeas/go-webfinger"
"github.com/writeas/web-core/log" "github.com/writeas/web-core/log"
"github.com/writeas/writefreely/config"
"github.com/writefreely/go-nodeinfo" "github.com/writefreely/go-nodeinfo"
"net/http" "net/http"
"path/filepath" "path/filepath"
@ -152,8 +153,13 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired)) write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired))
write.HandleFunc("/invite/{code}", handler.Web(handleViewInvite, UserLevelNoneRequired)).Methods("GET") write.HandleFunc("/invite/{code}", handler.Web(handleViewInvite, UserLevelNoneRequired)).Methods("GET")
// TODO: show a reader-specific 404 page if the function is disabled // TODO: show a reader-specific 404 page if the function is disabled
// TODO: change this based on configuration for either public or private-to-this-instance readPerm := func(cfg *config.Config) UserLevel {
readPerm := UserLevelOptional if cfg.App.Private {
// Private instance, so only allow users to access Reader routes
return UserLevelUserType
}
return UserLevelOptionalType
}
write.HandleFunc("/read", handler.Web(viewLocalTimeline, readPerm)) write.HandleFunc("/read", handler.Web(viewLocalTimeline, readPerm))
RouteRead(handler, readPerm, write.PathPrefix("/read").Subrouter()) RouteRead(handler, readPerm, write.PathPrefix("/read").Subrouter())
@ -173,8 +179,8 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
if apper.App().cfg.App.SingleUser { if apper.App().cfg.App.SingleUser {
RouteCollections(handler, write.PathPrefix("/").Subrouter()) RouteCollections(handler, write.PathPrefix("/").Subrouter())
} else { } else {
write.HandleFunc("/{prefix:[@~$!\\-+]}{collection}", handler.Web(handleViewCollection, UserLevelOptional)) write.HandleFunc("/{prefix:[@~$!\\-+]}{collection}", handler.Web(handleViewCollection, UserLevelReader))
write.HandleFunc("/{collection}/", handler.Web(handleViewCollection, UserLevelOptional)) write.HandleFunc("/{collection}/", handler.Web(handleViewCollection, UserLevelReader))
RouteCollections(handler, write.PathPrefix("/{prefix:[@~$!\\-+]?}{collection}").Subrouter()) RouteCollections(handler, write.PathPrefix("/{prefix:[@~$!\\-+]?}{collection}").Subrouter())
// Posts // Posts
} }
@ -184,19 +190,19 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
} }
func RouteCollections(handler *Handler, r *mux.Router) { func RouteCollections(handler *Handler, r *mux.Router) {
r.HandleFunc("/page/{page:[0-9]+}", handler.Web(handleViewCollection, UserLevelOptional)) r.HandleFunc("/page/{page:[0-9]+}", handler.Web(handleViewCollection, UserLevelReader))
r.HandleFunc("/tag:{tag}", handler.Web(handleViewCollectionTag, UserLevelOptional)) r.HandleFunc("/tag:{tag}", handler.Web(handleViewCollectionTag, UserLevelReader))
r.HandleFunc("/tag:{tag}/feed/", handler.Web(ViewFeed, UserLevelOptional)) r.HandleFunc("/tag:{tag}/feed/", handler.Web(ViewFeed, UserLevelReader))
r.HandleFunc("/tags/{tag}", handler.Web(handleViewCollectionTag, UserLevelOptional)) r.HandleFunc("/tags/{tag}", handler.Web(handleViewCollectionTag, UserLevelReader))
r.HandleFunc("/sitemap.xml", handler.All(handleViewSitemap)) r.HandleFunc("/sitemap.xml", handler.All(handleViewSitemap))
r.HandleFunc("/feed/", handler.All(ViewFeed)) r.HandleFunc("/feed/", handler.All(ViewFeed))
r.HandleFunc("/{slug}", handler.Web(viewCollectionPost, UserLevelOptional)) r.HandleFunc("/{slug}", handler.CollectionPostOrStatic)
r.HandleFunc("/{slug}/edit", handler.Web(handleViewPad, UserLevelUser)) r.HandleFunc("/{slug}/edit", handler.Web(handleViewPad, UserLevelUser))
r.HandleFunc("/{slug}/edit/meta", handler.Web(handleViewMeta, UserLevelUser)) r.HandleFunc("/{slug}/edit/meta", handler.Web(handleViewMeta, UserLevelUser))
r.HandleFunc("/{slug}/", handler.Web(handleCollectionPostRedirect, UserLevelOptional)).Methods("GET") r.HandleFunc("/{slug}/", handler.Web(handleCollectionPostRedirect, UserLevelReader)).Methods("GET")
} }
func RouteRead(handler *Handler, readPerm UserLevel, r *mux.Router) { func RouteRead(handler *Handler, readPerm UserLevelFunc, r *mux.Router) {
r.HandleFunc("/api/posts", handler.Web(viewLocalTimelineAPI, readPerm)) r.HandleFunc("/api/posts", handler.Web(viewLocalTimelineAPI, readPerm))
r.HandleFunc("/p/{page}", handler.Web(viewLocalTimeline, readPerm)) r.HandleFunc("/p/{page}", handler.Web(viewLocalTimeline, readPerm))
r.HandleFunc("/feed/", handler.Web(viewLocalTimelineFeed, readPerm)) r.HandleFunc("/feed/", handler.Web(viewLocalTimelineFeed, readPerm))