move auth middleware to auth package
This commit is contained in:
@@ -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_
|
||||
|
||||
+2
-1
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
+3
-112
@@ -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)
|
||||
}
|
||||
|
||||
+7
-7
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user