From 05f5c435fdea7725c981f4e579ba5a3bb1ca7005 Mon Sep 17 00:00:00 2001 From: Umputun Date: Mon, 25 Dec 2017 16:38:58 -0600 Subject: [PATCH] move auth middleware to auth package --- README.md | 49 +++++++++------ app/rest/admin.go | 3 +- app/rest/auth/middleware.go | 121 ++++++++++++++++++++++++++++++++++++ app/rest/middleware.go | 115 +--------------------------------- app/rest/server.go | 14 ++--- 5 files changed, 162 insertions(+), 140 deletions(-) create mode 100644 app/rest/auth/middleware.go diff --git a/README.md b/README.md index eb1f5cc1..906b49c0 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,16 @@ Comment engine - copy provided docker-compose.yml and customize for your needs - make sure you **don't keep** `DEV=true` for any non-development deployments -- pull and start `docker compose pull && docker compose up` +- pull and start `docker compose pull && docker compose up` + +#### Run modes + +- `server` activates regular, server mode +- `import` performs import from external providers (disqus and internal json, see `/api/v1/admin/export`) + +#### Register oauth2 providers + +TBD ### Frontend @@ -22,13 +31,13 @@ TBD - `GET /logout` - logout - `GET /api/v1/user` - get user info, _auth required_ -``` +```go type User struct { - Name string `json:"name"` - ID string `json:"id"` - Picture string `json:"picture"` - Profile string `json:"profile"` - Admin bool `json:"admin"` + Name string `json:"name"` + ID string `json:"id"` + Picture string `json:"picture"` + Profile string `json:"profile"` + Admin bool `json:"admin"` } ``` @@ -38,26 +47,26 @@ _currently supported providers are `google` and `github`_ - `POST /api/v1/comment` - add a comment. _auth required_ -``` +```go type Comment struct { - ID string `json:"id"` // read only - ParentID string `json:"pid"` - Text string `json:"text"` - User User `json:"user"` // read only - Locator Locator `json:"locator"` - Score int `json:"score"` // read only - Votes map[string]bool `json:"votes"` // read only - Timestamp time.Time `json:"time"` // read only + ID string `json:"id"` // read only + ParentID string `json:"pid"` + Text string `json:"text"` + User User `json:"user"` // read only + Locator Locator `json:"locator"` + Score int `json:"score"` // read only + Votes map[string]bool `json:"votes"` // read only + Timestamp time.Time `json:"time"` // read only } type Locator struct { - SiteID string `json:"site"` - URL string `json:"url"` + SiteID string `json:"site"` + URL string `json:"url"` } ``` -- `GET /api/v1/find?url=post-url` - find all comments for given post returns list of `Comment` -- `GET /api/v1/last/{max}` - get last `{max}` comments +- `GET /api/v1/find?url=post-url` - find all comments for given post, returns flat list of `Comment` +- `GET /api/v1/last/{max}` - get up to `{max}` last comments - `GET /api/v1/id/{id}` - get comment by `id` - `GET /api/v1/count?url=post-url` - get comment's count for `{url}` - `PUT /api/v1/vote/{id}?url=post-url&vote=1` - vote for comment. `vote`=1 will increase score, -1 decreases. _auth required_ diff --git a/app/rest/admin.go b/app/rest/admin.go index d768ca35..dd2396ba 100644 --- a/app/rest/admin.go +++ b/app/rest/admin.go @@ -8,6 +8,7 @@ import ( "github.com/go-chi/render" "github.com/umputun/remark/app/migrator" + "github.com/umputun/remark/app/rest/auth" "github.com/umputun/remark/app/store" ) @@ -19,7 +20,7 @@ type admin struct { func (a *admin) routes() chi.Router { router := chi.NewRouter() - router.Use(AdminOnly) + router.Use(auth.AdminOnly) router.Delete("/comment/{id}", a.deleteCommentCtrl) router.Put("/user/{userid}", a.setBlockCtrl) router.Get("/export", a.exportCtrl) diff --git a/app/rest/auth/middleware.go b/app/rest/auth/middleware.go new file mode 100644 index 00000000..ec089605 --- /dev/null +++ b/app/rest/auth/middleware.go @@ -0,0 +1,121 @@ +package auth + +import ( + "context" + "errors" + "net/http" + + "github.com/gorilla/sessions" + + "github.com/umputun/remark/app/store" +) + +type contextKey string + +// Mode defines behavior of Auth middleware +type Mode int + +// auth modes +const ( + Anonymous Mode = iota // propagates user info only, doesn't protect resource + Developer // fake dev auth, admin too + Full // real auth +) + +// Auth middleware adds auth from session and populates user info +func Auth(sessionStore *sessions.FilesystemStore, admins []string, modes ...Mode) func(http.Handler) http.Handler { + + inModes := func(mode Mode) bool { + for _, m := range modes { + if m == mode { + return true + } + } + return false + } + + f := func(h http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + + // for dev mode skip all real auth, make dev admin user + if inModes(Developer) { + user := store.User{ + ID: "dev", + Name: "developer one", + Picture: "https://friends.radio-t.com/resources/images/rt_logo_64.png", + Profile: "https://radio-t.com/info/", + Admin: true, + } + ctx := r.Context() + ctx = context.WithValue(ctx, contextKey("user"), user) + r = r.WithContext(ctx) + h.ServeHTTP(w, r) + return + } + + session, err := sessionStore.Get(r, "remark") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + uinfoData, ok := session.Values["uinfo"] + if !ok && inModes(Full) { // return StatusUnauthorized for full auth mode only + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + if ok { // if uinfo in session, populate to context + user := uinfoData.(store.User) + for _, admin := range admins { + if admin == user.ID { + user.Admin = true + break + } + } + + ctx := r.Context() + ctx = context.WithValue(ctx, contextKey("user"), user) + r = r.WithContext(ctx) + } + h.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) + } + return f +} + +// AdminOnly allows access to admins +func AdminOnly(next http.Handler) http.Handler { + fn := func(w http.ResponseWriter, r *http.Request) { + + user, err := GetUserInfo(r) + if err != nil { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + if !user.Admin { + http.Error(w, "Access denied", http.StatusForbidden) + return + } + + next.ServeHTTP(w, r) + } + return http.HandlerFunc(fn) +} + +// GetUserInfo extracts user, or and token from request's context +func GetUserInfo(r *http.Request) (user store.User, err error) { + + ctx := r.Context() + if ctx == nil { + return store.User{}, errors.New("user not defined") + } + + if u, ok := ctx.Value(contextKey("user")).(store.User); ok { + return u, nil + } + + return store.User{}, errors.New("user can't be parsed") +} diff --git a/app/rest/middleware.go b/app/rest/middleware.go index 577f499e..4c9dd78c 100644 --- a/app/rest/middleware.go +++ b/app/rest/middleware.go @@ -2,7 +2,6 @@ package rest import ( "bytes" - "context" "fmt" "io/ioutil" "log" @@ -17,10 +16,8 @@ import ( "github.com/didip/tollbooth" "github.com/go-chi/chi/middleware" "github.com/go-chi/render" - "github.com/go-errors/errors" - "github.com/gorilla/sessions" - "github.com/umputun/remark/app/store" + "github.com/umputun/remark/app/rest/auth" ) var org = "Umputun" @@ -102,7 +99,7 @@ func Recoverer(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { defer func() { if rvr := recover(); rvr != nil { - log.Printf("[ERROR] request panic, %v", rvr) + log.Printf("[WARN] request panic, %v", rvr) debug.PrintStack() http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) } @@ -114,112 +111,6 @@ func Recoverer(next http.Handler) http.Handler { return http.HandlerFunc(fn) } -type contextKey string - -const ( - anonymous = iota - developer - full -) - -// Auth adds auth from session and populate user info -func Auth(sessionStore *sessions.FilesystemStore, admins []string, modes ...int) func(http.Handler) http.Handler { - - inModes := func(mode int) bool { - for _, m := range modes { - if m == mode { - return true - } - } - return false - } - - f := func(h http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - - // for dev mode skip all real auth, make dev admin user - if inModes(developer) { - user := store.User{ - ID: "dev", - Name: "developer one", - Picture: "https://friends.radio-t.com/resources/images/rt_logo_64.png", - Profile: "https://radio-t.com/info/", - Admin: true, - } - ctx := r.Context() - ctx = context.WithValue(ctx, contextKey("user"), user) - r = r.WithContext(ctx) - h.ServeHTTP(w, r) - return - } - - session, err := sessionStore.Get(r, "remark") - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - - uinfoData, ok := session.Values["uinfo"] - if !ok && inModes(full) { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - if ok { - user := uinfoData.(store.User) - for _, admin := range admins { - if admin == user.ID { - user.Admin = true - break - } - } - - ctx := r.Context() - ctx = context.WithValue(ctx, contextKey("user"), user) - r = r.WithContext(ctx) - } - h.ServeHTTP(w, r) - } - return http.HandlerFunc(fn) - } - return f -} - -// AdminOnly allows access to admins -func AdminOnly(next http.Handler) http.Handler { - fn := func(w http.ResponseWriter, r *http.Request) { - - user, err := GetUserInfo(r) - if err != nil { - http.Error(w, "Unauthorized", http.StatusUnauthorized) - return - } - - if !user.Admin { - http.Error(w, "Access denied", http.StatusForbidden) - return - } - - next.ServeHTTP(w, r) - } - return http.HandlerFunc(fn) -} - -// GetUserInfo extracts user, or and token from request's context -func GetUserInfo(r *http.Request) (user store.User, err error) { - - ctx := r.Context() - if ctx == nil { - return store.User{}, errors.New("user not defined") - } - - if u, ok := ctx.Value(contextKey("user")).(store.User); ok { - return u, nil - } - - return store.User{}, errors.New("user can't be parsed") -} - // LoggerFlag type type LoggerFlag int @@ -273,7 +164,7 @@ func Logger(flags ...LoggerFlag) func(http.Handler) http.Handler { } if inFlags(LogUser) { - u, err := GetUserInfo(r) + u, err := auth.GetUserInfo(r) if err == nil && u.Name != "" { user = fmt.Sprintf(" - %s %q", u.ID, u.Name) } diff --git a/app/rest/server.go b/app/rest/server.go index 1503c88d..86b771df 100644 --- a/app/rest/server.go +++ b/app/rest/server.go @@ -39,10 +39,10 @@ type Server struct { func (s *Server) Run() { log.Print("[INFO] activate rest server") - applyDevMode := func(mode int) (modes []int) { + applyDevMode := func(mode auth.Mode) (modes []auth.Mode) { modes = append(modes, mode) if s.DevMode { - modes = append(modes, developer) + modes = append(modes, auth.Developer) } return modes } @@ -50,7 +50,7 @@ func (s *Server) Run() { router := chi.NewRouter() router.Use(middleware.RealIP, Recoverer) router.Use(middleware.Throttle(1000), middleware.Timeout(60*time.Second)) - router.Use(Auth(s.SessionStore, s.Admins, applyDevMode(anonymous)...)) + router.Use(auth.Auth(s.SessionStore, s.Admins, applyDevMode(auth.Anonymous)...)) router.Use(Limiter(10), AppInfo("remark", s.Version), Ping, Logger(LogAll)) router.Get("/login/google", s.AuthGoogle.LoginHandler) @@ -65,7 +65,7 @@ func (s *Server) Run() { rapi.Get("/last/{max}", s.lastCommentsCtrl) rapi.Get("/count", s.countCtrl) - rapi.With(Auth(s.SessionStore, s.Admins, applyDevMode(full)...)).Group(func(rauth chi.Router) { + rapi.With(auth.Auth(s.SessionStore, s.Admins, applyDevMode(auth.Full)...)).Group(func(rauth chi.Router) { rauth.Post("/comment", s.createCommentCtrl) rauth.Get("/user", s.userInfoCtrl) rauth.Put("/vote/{id}", s.voteCtrl) @@ -110,7 +110,7 @@ func (s *Server) createCommentCtrl(w http.ResponseWriter, r *http.Request) { return } - user, err := GetUserInfo(r) + user, err := auth.GetUserInfo(r) if err != nil { // this not suppose to happen (handled by Auth), just dbl-check httpError(w, r, http.StatusUnauthorized, err, "can't get user info") return @@ -213,7 +213,7 @@ func (s *Server) commentByIDCtrl(w http.ResponseWriter, r *http.Request) { // GET /user func (s *Server) userInfoCtrl(w http.ResponseWriter, r *http.Request) { - user, err := GetUserInfo(r) + user, err := auth.GetUserInfo(r) if err != nil { httpError(w, r, http.StatusUnauthorized, err, "can't get user info") return @@ -235,7 +235,7 @@ func (s *Server) countCtrl(w http.ResponseWriter, r *http.Request) { // PUT /vote/{id}?url=post-url&vote=1 func (s *Server) voteCtrl(w http.ResponseWriter, r *http.Request) { - user, err := GetUserInfo(r) + user, err := auth.GetUserInfo(r) if err != nil { httpError(w, r, http.StatusUnauthorized, err, "can't get user info") return