From b3a36a3be722cb8e96fb391787237f8644a547a6 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Sun, 16 Jun 2019 18:55:50 -0400 Subject: [PATCH] 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 --- config/config.go | 4 ++- handle.go | 77 +++++++++++++++++++++++++++++++++++------------- posts.go | 16 ++++++---- routes.go | 28 +++++++++++------- 4 files changed, 87 insertions(+), 38 deletions(-) diff --git a/config/config.go b/config/config.go index add5447..8009208 100644 --- a/config/config.go +++ b/config/config.go @@ -76,7 +76,9 @@ type ( // Federation Federation bool `ini:"federation"` PublicStats bool `ini:"public_stats"` - Private bool `ini:"private"` + + // Access + Private bool `ini:"private"` // Additional functions LocalTimeline bool `ini:"local_timeline"` diff --git a/handle.go b/handle.go index 81a4823..c70859e 100644 --- a/handle.go +++ b/handle.go @@ -23,24 +23,52 @@ import ( "github.com/gorilla/sessions" "github.com/writeas/impart" "github.com/writeas/web-core/log" + "github.com/writeas/writefreely/config" "github.com/writeas/writefreely/page" ) +// UserLevel represents the required user level for accessing an endpoint type UserLevel int const ( - UserLevelNone UserLevel = iota // user or not -- ignored - UserLevelOptional // user or not -- object fetched if user - UserLevelNoneRequired // non-user (required) - UserLevelUser // user (required) + UserLevelNoneType UserLevel = iota // user or not -- ignored + UserLevelOptionalType // user or not -- object fetched if user + UserLevelNoneRequiredType // non-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 ( handlerFunc func(app *App, 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 dataHandlerFunc func(app *App, w http.ResponseWriter, r *http.Request) ([]byte, string, error) authFunc func(app *App, r *http.Request) (*User, error) + UserLevelFunc func(cfg *config.Config) UserLevel ) type Handler struct { @@ -307,7 +335,7 @@ func (h *Handler) Page(n string) http.HandlerFunc { }, 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) { // TODO: factor out this logic shared with Web() 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 err error - if ul != UserLevelNone { + if ul(h.app.App().cfg) != UserLevelNoneType { 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 log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul, err) } _, gotUser := session.Values[cookieUserVal].(*User) - if ul == UserLevelNoneRequired && gotUser { + if ul(h.app.App().cfg) == UserLevelNoneRequiredType && gotUser { to := correctPageFromLoginAttempt(r) log.Info("Handler: Required NO user, but got one. Redirecting to %s", to) err := impart.HTTPError{http.StatusFound, to} status = err.Status 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.") err := ErrNotLoggedIn 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- // 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) { h.handleHTTPError(w, r, func() error { 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()) }() - if ul != UserLevelNone { + if ul(h.app.App().cfg) != UserLevelNoneType { 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 log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul, err) } _, gotUser := session.Values[cookieUserVal].(*User) - if ul == UserLevelNoneRequired && gotUser { + if ul(h.app.App().cfg) == UserLevelNoneRequiredType && gotUser { to := correctPageFromLoginAttempt(r) log.Info("Handler: Required NO user, but got one. Redirecting to %s", to) err := impart.HTTPError{http.StatusFound, to} status = err.Status 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.") err := ErrNotLoggedIn 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) { h.handleHTTPError(w, r, func() error { 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) { h.handleHTTPError(w, r, func() error { start := time.Now() var status int - if ul != UserLevelNone { + if ul(h.app.App().cfg) != UserLevelNoneType { 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 log.Error("Handler: Unable to get session (for user permission %d); ignoring: %v", ul, err) } _, gotUser := session.Values[cookieUserVal].(*User) - if ul == UserLevelNoneRequired && gotUser { + if ul(h.app.App().cfg) == UserLevelNoneRequiredType && gotUser { to := correctPageFromLoginAttempt(r) log.Info("Handler: Required NO user, but got one. Redirecting to %s", to) err := impart.HTTPError{http.StatusFound, to} status = err.Status 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.") err := ErrNotLoggedIn status = err.Status diff --git a/posts.go b/posts.go index 0efa5ec..7fd6635 100644 --- a/posts.go +++ b/posts.go @@ -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 { vars := mux.Vars(r) slug := vars["slug"] @@ -1199,12 +1209,6 @@ func viewCollectionPost(app *App, w http.ResponseWriter, r *http.Request) error isMarkdown := strings.HasSuffix(slug, ".md") 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{} err := processCollectionRequest(cr, vars, w, r) if err != nil { diff --git a/routes.go b/routes.go index 13dd3a5..58bd455 100644 --- a/routes.go +++ b/routes.go @@ -14,6 +14,7 @@ import ( "github.com/gorilla/mux" "github.com/writeas/go-webfinger" "github.com/writeas/web-core/log" + "github.com/writeas/writefreely/config" "github.com/writefreely/go-nodeinfo" "net/http" "path/filepath" @@ -152,8 +153,13 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { write.HandleFunc("/login", handler.Web(viewLogin, UserLevelNoneRequired)) write.HandleFunc("/invite/{code}", handler.Web(handleViewInvite, UserLevelNoneRequired)).Methods("GET") // 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 := UserLevelOptional + readPerm := func(cfg *config.Config) UserLevel { + 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)) 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 { RouteCollections(handler, write.PathPrefix("/").Subrouter()) } else { - write.HandleFunc("/{prefix:[@~$!\\-+]}{collection}", handler.Web(handleViewCollection, UserLevelOptional)) - write.HandleFunc("/{collection}/", handler.Web(handleViewCollection, UserLevelOptional)) + write.HandleFunc("/{prefix:[@~$!\\-+]}{collection}", handler.Web(handleViewCollection, UserLevelReader)) + write.HandleFunc("/{collection}/", handler.Web(handleViewCollection, UserLevelReader)) RouteCollections(handler, write.PathPrefix("/{prefix:[@~$!\\-+]?}{collection}").Subrouter()) // Posts } @@ -184,19 +190,19 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router { } func RouteCollections(handler *Handler, r *mux.Router) { - r.HandleFunc("/page/{page:[0-9]+}", handler.Web(handleViewCollection, UserLevelOptional)) - r.HandleFunc("/tag:{tag}", handler.Web(handleViewCollectionTag, UserLevelOptional)) - r.HandleFunc("/tag:{tag}/feed/", handler.Web(ViewFeed, UserLevelOptional)) - r.HandleFunc("/tags/{tag}", handler.Web(handleViewCollectionTag, UserLevelOptional)) + r.HandleFunc("/page/{page:[0-9]+}", handler.Web(handleViewCollection, UserLevelReader)) + r.HandleFunc("/tag:{tag}", handler.Web(handleViewCollectionTag, UserLevelReader)) + r.HandleFunc("/tag:{tag}/feed/", handler.Web(ViewFeed, UserLevelReader)) + r.HandleFunc("/tags/{tag}", handler.Web(handleViewCollectionTag, UserLevelReader)) r.HandleFunc("/sitemap.xml", handler.All(handleViewSitemap)) 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/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("/p/{page}", handler.Web(viewLocalTimeline, readPerm)) r.HandleFunc("/feed/", handler.Web(viewLocalTimelineFeed, readPerm))