dev mode mixed with normal auth via basic
This commit is contained in:
+5
-3
@@ -27,8 +27,9 @@ var opts struct {
|
||||
RemarkURL string `long:"url" env:"REMARK_URL" default:"https://remark42.com" description:"url to remark"`
|
||||
Admins []string `long:"admin" env:"ADMIN" description:"admin(s) names" env-delim:","`
|
||||
|
||||
DevMode bool `long:"dev" env:"DEV" description:"development mode, no auth enforced"`
|
||||
Dbg bool `long:"dbg" env:"DEBUG" description:"debug mode"`
|
||||
DevMode bool `long:"dev" env:"DEV" description:"development mode, no auth enforced"`
|
||||
DevPasswd string `long:"dev-passwd" env:"DEV_PASSWD" default:"password" description:"development mode password"`
|
||||
Dbg bool `long:"dbg" env:"DEBUG" description:"debug mode"`
|
||||
|
||||
BackupLocation string `long:"backup" env:"BACKUP_PATH" default:"./var" description:"backups location"`
|
||||
MaxBackupFiles int `long:"max-back" env:"MAX_BACKUP_FILES" default:"10" description:"max backups to keep"`
|
||||
@@ -108,13 +109,14 @@ func main() {
|
||||
srv := server.Rest{
|
||||
Version: revision,
|
||||
DataService: dataService,
|
||||
DevMode: opts.DevMode,
|
||||
Exporter: &exporter,
|
||||
Authenticator: auth.Authenticator{
|
||||
Admins: opts.Admins,
|
||||
SessionStore: sessionStore,
|
||||
Providers: makeAuthProviders(sessionStore, avatarProxy),
|
||||
AvatarProxy: avatarProxy,
|
||||
DevEnabled: opts.DevMode,
|
||||
DevPasswd: opts.DevPasswd,
|
||||
},
|
||||
Cache: rest.NewLoadingCache(4*time.Hour, 15*time.Minute, postFlushFn),
|
||||
Notifier: notifier.NewNoOperation(),
|
||||
|
||||
+42
-26
@@ -2,7 +2,9 @@ package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
|
||||
@@ -16,18 +18,11 @@ type Authenticator struct {
|
||||
AvatarProxy *AvatarProxy
|
||||
Admins []string
|
||||
Providers []Provider
|
||||
|
||||
DevEnabled bool
|
||||
DevPasswd 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
|
||||
)
|
||||
|
||||
var devUser = store.User{
|
||||
ID: "dev",
|
||||
Name: "developer one",
|
||||
@@ -37,22 +32,13 @@ var devUser = store.User{
|
||||
}
|
||||
|
||||
// Auth middleware adds auth from session and populates user info
|
||||
func (a *Authenticator) Auth(modes []Mode) func(http.Handler) http.Handler {
|
||||
|
||||
inModes := func(mode Mode) bool {
|
||||
for _, m := range modes {
|
||||
if m == mode {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
func (a *Authenticator) Auth(reqAuth bool) func(http.Handler) http.Handler {
|
||||
|
||||
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) {
|
||||
// dev user - skip regular auth check and populate dev to context
|
||||
if a.basicDevUser(w, r) {
|
||||
user := devUser
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, rest.ContextKey("user"), user)
|
||||
@@ -62,18 +48,18 @@ func (a *Authenticator) Auth(modes []Mode) func(http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
session, err := a.SessionStore.Get(r, "remark")
|
||||
if err != nil && inModes(Full) { // in full auth lack of session causes Unauthorized
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
if err != nil && reqAuth { // in full auth lack of session causes Unauthorized
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil { // in any other mode just pass it to next handler
|
||||
if err != nil { // in anonymous mode just pass it to next handler
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
uinfoData, ok := session.Values["uinfo"]
|
||||
if !ok && inModes(Full) { // return StatusUnauthorized for full auth mode only
|
||||
if !ok && reqAuth {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
@@ -117,3 +103,33 @@ func (a *Authenticator) AdminOnly(next http.Handler) http.Handler {
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
func (a *Authenticator) basicDevUser(w http.ResponseWriter, r *http.Request) bool {
|
||||
|
||||
if a.DevPasswd == "" || !a.DevEnabled {
|
||||
return false
|
||||
}
|
||||
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
|
||||
s := strings.SplitN(r.Header.Get("Authorization"), " ", 2)
|
||||
if len(s) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
b, err := base64.StdEncoding.DecodeString(s[1])
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
pair := strings.SplitN(string(b), ":", 2)
|
||||
if len(pair) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
if pair[0] != "dev" || pair[1] != a.DevPasswd {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -130,10 +130,8 @@ func (a *admin) checkBlocked(siteID string, user store.User) bool {
|
||||
func (a *admin) maskInfo(comments []store.Comment, r *http.Request) (res []store.Comment) {
|
||||
res = make([]store.Comment, len(comments))
|
||||
|
||||
isAdmin := false
|
||||
if user, err := rest.GetUserInfo(r); err == nil && user.Admin { // make seprate cache key for admins
|
||||
isAdmin = true
|
||||
}
|
||||
user, err := rest.GetUserInfo(r)
|
||||
isAdmin := (err == nil && user.Admin) // make seprate cache key for admins
|
||||
|
||||
for i, c := range comments {
|
||||
|
||||
|
||||
@@ -29,7 +29,8 @@ func TestAdmin_Delete(t *testing.T) {
|
||||
|
||||
client := http.Client{}
|
||||
req, err := http.NewRequest(http.MethodDelete,
|
||||
fmt.Sprintf("http://127.0.0.1:%d/api/v1/admin/comment/%s?site=radio-t&url=https://radio-t.com/blah", port, id1), nil)
|
||||
fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/admin/comment/%s?site=radio-t&url=https://radio-t.com/blah",
|
||||
port, id1), nil)
|
||||
assert.Nil(t, err)
|
||||
resp, err := client.Do(req)
|
||||
assert.Nil(t, err)
|
||||
@@ -60,8 +61,7 @@ func TestAdmin_Pin(t *testing.T) {
|
||||
pin := func(val int) int {
|
||||
client := http.Client{}
|
||||
req, err := http.NewRequest(http.MethodPut,
|
||||
fmt.Sprintf("http://127.0.0.1:%d/api/v1/admin/pin/%s?site=radio-t&url=https://radio-t.com/blah&pin=%d", port, id1, val),
|
||||
nil)
|
||||
fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/admin/pin/%s?site=radio-t&url=https://radio-t.com/blah&pin=%d", port, id1, val), nil)
|
||||
assert.Nil(t, err)
|
||||
resp, err := client.Do(req)
|
||||
assert.Nil(t, err)
|
||||
@@ -106,8 +106,7 @@ func TestAdmin_Block(t *testing.T) {
|
||||
block := func(val int) (code int, body []byte) {
|
||||
client := http.Client{}
|
||||
req, err := http.NewRequest(http.MethodPut,
|
||||
fmt.Sprintf("http://127.0.0.1:%d/api/v1/admin/user/%s?site=radio-t&block=%d",
|
||||
port, "user1", val), nil)
|
||||
fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/admin/user/%s?site=radio-t&block=%d", port, "user1", val), nil)
|
||||
assert.Nil(t, err)
|
||||
resp, err := client.Do(req)
|
||||
require.Nil(t, err)
|
||||
@@ -154,7 +153,7 @@ func TestAdmin_Export(t *testing.T) {
|
||||
addComment(t, c1, port)
|
||||
addComment(t, c2, port)
|
||||
|
||||
body, code := get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/admin/export?site=radio-t&mode=stream", port))
|
||||
body, code := get(t, fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/admin/export?site=radio-t&mode=stream", port))
|
||||
assert.Equal(t, 200, code)
|
||||
assert.Equal(t, 2, strings.Count(body, "\n"))
|
||||
assert.Equal(t, 2, strings.Count(body, "\"text\""))
|
||||
|
||||
+2
-12
@@ -31,7 +31,6 @@ import (
|
||||
// Rest is a rest access server
|
||||
type Rest struct {
|
||||
Version string
|
||||
DevMode bool
|
||||
|
||||
DataService store.Service
|
||||
Authenticator auth.Authenticator
|
||||
@@ -47,15 +46,6 @@ type Rest struct {
|
||||
func (s *Rest) Run(port int) {
|
||||
log.Print("[INFO] activate rest server")
|
||||
|
||||
// add auth.Developer flag if dev mode is active
|
||||
maybeWithDevMode := func(mode auth.Mode) (modes []auth.Mode) {
|
||||
modes = append(modes, mode)
|
||||
if s.DevMode {
|
||||
modes = append(modes, auth.Developer)
|
||||
}
|
||||
return modes
|
||||
}
|
||||
|
||||
if len(s.Authenticator.Admins) > 0 {
|
||||
log.Printf("[DEBUG] admins %+v", s.Authenticator.Admins)
|
||||
}
|
||||
@@ -66,7 +56,7 @@ func (s *Rest) Run(port int) {
|
||||
router.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil)))
|
||||
|
||||
// all request by default allow anonymous access
|
||||
router.Use(s.Authenticator.Auth(maybeWithDevMode(auth.Anonymous)))
|
||||
router.Use(s.Authenticator.Auth(false))
|
||||
|
||||
router.Use(AppInfo("remark42", s.Version), Ping, Logger(LogAll))
|
||||
router.Use(context.ClearHandler) // if you aren't using gorilla/mux, you need to wrap your handlers with context.ClearHandler
|
||||
@@ -97,7 +87,7 @@ func (s *Rest) Run(port int) {
|
||||
rapi.Get("/config", s.configCtrl)
|
||||
|
||||
// protected routes, require auth
|
||||
rapi.With(s.Authenticator.Auth(maybeWithDevMode(auth.Full))).Group(func(rauth chi.Router) {
|
||||
rapi.With(s.Authenticator.Auth(true)).Group(func(rauth chi.Router) {
|
||||
rauth.Post("/comment", s.createCommentCtrl)
|
||||
rauth.Put("/comment/{id}", s.updateCommentCtrl)
|
||||
rauth.Get("/user", s.userInfoCtrl)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
@@ -40,7 +41,7 @@ func TestServer_Create(t *testing.T) {
|
||||
defer cleanup(srv)
|
||||
|
||||
r := strings.NewReader(`{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`)
|
||||
resp, err := http.Post(fmt.Sprintf("http://127.0.0.1:%d/api/v1/comment", port), "application/json", r)
|
||||
resp, err := http.Post(fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/comment", port), "application/json", r)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
|
||||
@@ -62,7 +63,7 @@ func TestServer_CreateAndGet(t *testing.T) {
|
||||
|
||||
// create comment
|
||||
r := strings.NewReader(`{"text": "**test** *123* http://radio-t.com", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`)
|
||||
resp, err := http.Post(fmt.Sprintf("http://127.0.0.1:%d/api/v1/comment", port), "application/json", r)
|
||||
resp, err := http.Post(fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/comment", port), "application/json", r)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
@@ -74,7 +75,7 @@ func TestServer_CreateAndGet(t *testing.T) {
|
||||
id := c["id"].(string)
|
||||
|
||||
// get created comment by id
|
||||
res, code := get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", port, id))
|
||||
res, code := get(t, fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", port, id))
|
||||
assert.Equal(t, 200, code)
|
||||
comment := store.Comment{}
|
||||
err = json.Unmarshal([]byte(res), &comment)
|
||||
@@ -135,7 +136,7 @@ func TestServer_Update(t *testing.T) {
|
||||
|
||||
client := http.Client{}
|
||||
req, err := http.NewRequest(http.MethodPut,
|
||||
fmt.Sprintf("http://127.0.0.1:%d/api/v1/comment/"+id+"?site=radio-t&url=https://radio-t.com/blah1", port),
|
||||
fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/comment/"+id+"?site=radio-t&url=https://radio-t.com/blah1", port),
|
||||
strings.NewReader(`{"text":"updated text", "summary":"my edit"}`))
|
||||
assert.Nil(t, err)
|
||||
b, err := client.Do(req)
|
||||
@@ -154,7 +155,7 @@ func TestServer_Update(t *testing.T) {
|
||||
assert.True(t, time.Since(c2.Edit.Timestamp) < 1*time.Second)
|
||||
|
||||
// read updated comment
|
||||
res, code := get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", port, id))
|
||||
res, code := get(t, fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", port, id))
|
||||
assert.Equal(t, 200, code)
|
||||
c3 := store.Comment{}
|
||||
err = json.Unmarshal([]byte(res), &c3)
|
||||
@@ -230,7 +231,7 @@ func TestServer_UserInfo(t *testing.T) {
|
||||
assert.NotNil(t, srv)
|
||||
defer cleanup(srv)
|
||||
|
||||
body, code := get(t, fmt.Sprintf("http://127.0.0.1:%d/api/v1/user?site=radio-t", port))
|
||||
body, code := get(t, fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/user?site=radio-t", port))
|
||||
assert.Equal(t, 200, code)
|
||||
user := store.User{}
|
||||
err := json.Unmarshal([]byte(body), &user)
|
||||
@@ -256,8 +257,8 @@ func TestServer_Vote(t *testing.T) {
|
||||
vote := func(val int) int {
|
||||
client := http.Client{}
|
||||
req, err := http.NewRequest(http.MethodPut,
|
||||
fmt.Sprintf("http://127.0.0.1:%d/api/v1/vote/%s?site=radio-t&url=https://radio-t.com/blah&vote=%d", port, id1, val),
|
||||
nil)
|
||||
fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/vote/%s?site=radio-t&url=https://radio-t.com/blah&vote=%d",
|
||||
port, id1, val), nil)
|
||||
assert.Nil(t, err)
|
||||
resp, err := client.Do(req)
|
||||
assert.Nil(t, err)
|
||||
@@ -344,10 +345,12 @@ func prep(t *testing.T) (srv *Rest, port int) {
|
||||
require.Nil(t, err)
|
||||
srv = &Rest{
|
||||
DataService: store.Service{Interface: dataStore, EditDuration: 5 * time.Minute},
|
||||
DevMode: true,
|
||||
Authenticator: auth.Authenticator{
|
||||
Providers: nil,
|
||||
AvatarProxy: &auth.AvatarProxy{StorePath: "/tmp", RoutePath: "/api/v1/avatar"},
|
||||
SessionStore: sessions.NewFilesystemStore("/tmp", []byte("blah")),
|
||||
DevEnabled: true,
|
||||
DevPasswd: "password",
|
||||
Providers: nil,
|
||||
AvatarProxy: &auth.AvatarProxy{StorePath: "/tmp", RoutePath: "/api/v1/avatar"},
|
||||
},
|
||||
Exporter: &migrator.Remark{DataStore: dataStore},
|
||||
Cache: &mockCache{},
|
||||
@@ -374,7 +377,7 @@ func addComment(t *testing.T, c store.Comment, port int) string {
|
||||
|
||||
b, err := json.Marshal(c)
|
||||
assert.Nil(t, err, "can't marshal comment %+v", c)
|
||||
resp, err := http.Post(fmt.Sprintf("http://127.0.0.1:%d/api/v1/comment", port), "application/json", bytes.NewBuffer(b))
|
||||
resp, err := http.Post(fmt.Sprintf("http://dev:password@127.0.0.1:%d/api/v1/comment", port), "application/json", bytes.NewBuffer(b))
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
b, err = ioutil.ReadAll(resp.Body)
|
||||
|
||||
Reference in New Issue
Block a user