Feature/admin-migrator (#198)
* auth with a secret as admin * get rid of separate migrator service * clean sanitize dbg prints * fix dbg logging and set migrator routes
This commit is contained in:
@@ -23,4 +23,4 @@ deploy:
|
||||
docker cp remark42.bin:/artifacts/remark42.windows-amd64.zip bin/remark42.windows-amd64.zip
|
||||
docker rm -f remark42.bin
|
||||
|
||||
|
||||
.PHONY: bin
|
||||
@@ -20,7 +20,7 @@ type BackupCommand struct {
|
||||
Site string `long:"site" env:"SITE" default:"remark" description:"site name"`
|
||||
SharedSecret string `long:"secret" env:"SECRET" description:"shared secret key" required:"true"`
|
||||
Timeout time.Duration `long:"timeout" default:"15m" description:"import timeout"`
|
||||
URL string `long:"url" default:"http://127.0.0.1:8081" description:"migrator base url"`
|
||||
URL string `long:"url" default:"http://127.0.0.1:8080" description:"base url"`
|
||||
}
|
||||
|
||||
// Execute runs export with ExportCommand parameters, entry point for "export" command
|
||||
@@ -40,7 +40,7 @@ func (ec *BackupCommand) Execute(args []string) error {
|
||||
client := http.Client{}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), ec.Timeout)
|
||||
defer cancel()
|
||||
exportURL := fmt.Sprintf("%s/api/v1/admin/export?site=%s&secret=%s", ec.URL, ec.Site, ec.SharedSecret)
|
||||
exportURL := fmt.Sprintf("%s/api/v1/admin/export?mode=file&site=%s&secret=%s", ec.URL, ec.Site, ec.SharedSecret)
|
||||
req, err := http.NewRequest(http.MethodGet, exportURL, nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "can't make export request for %s", exportURL)
|
||||
|
||||
@@ -23,7 +23,7 @@ type ImportCommand struct {
|
||||
SharedSecret string `long:"secret" env:"SECRET" description:"shared secret key" required:"true"`
|
||||
|
||||
Timeout time.Duration `long:"timeout" default:"15m" description:"import timeout"`
|
||||
URL string `long:"url" default:"http://127.0.0.1:8081" description:"migrator base url"`
|
||||
URL string `long:"url" default:"http://127.0.0.1:8080" description:"migrator base url"`
|
||||
}
|
||||
|
||||
// Execute runs import with ImportCommand parameters, entry point for "import" command
|
||||
|
||||
@@ -213,7 +213,6 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) {
|
||||
exporter := &migrator.Remark{DataStore: dataService}
|
||||
|
||||
migr := &api.Migrator{
|
||||
Version: Revision,
|
||||
Cache: loadingCache,
|
||||
NativeImporter: &migrator.Remark{DataStore: dataService},
|
||||
DisqusImporter: &migrator.Disqus{DataStore: dataService},
|
||||
@@ -229,12 +228,12 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) {
|
||||
srv := &api.Rest{
|
||||
Version: Revision,
|
||||
DataService: dataService,
|
||||
Exporter: exporter,
|
||||
WebRoot: s.WebRoot,
|
||||
RemarkURL: s.RemarkURL,
|
||||
ImageProxy: imgProxy,
|
||||
CommentFormatter: commentFormatter,
|
||||
AvatarProxy: avatarProxy,
|
||||
Migrator: migr,
|
||||
ReadOnlyAge: s.ReadOnlyAge,
|
||||
SharedSecret: s.SharedSecret,
|
||||
Authenticator: auth.Authenticator{
|
||||
@@ -243,6 +242,7 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) {
|
||||
Providers: authProviders,
|
||||
DevPasswd: s.DevPasswd,
|
||||
PermissionChecker: dataService,
|
||||
KeyStore: keyStore,
|
||||
},
|
||||
Cache: loadingCache,
|
||||
}
|
||||
@@ -275,7 +275,6 @@ func (a *serverApp) run(ctx context.Context) error {
|
||||
// shutdown on context cancellation
|
||||
<-ctx.Done()
|
||||
a.restSrv.Shutdown()
|
||||
a.migratorSrv.Shutdown()
|
||||
if a.devAuth != nil {
|
||||
a.devAuth.Shutdown()
|
||||
}
|
||||
@@ -284,8 +283,7 @@ func (a *serverApp) run(ctx context.Context) error {
|
||||
}
|
||||
|
||||
}()
|
||||
a.activateBackup(ctx) // runs in goroutine for each site
|
||||
go a.migratorSrv.Run(a.Port + 1) // migrator server runs on +1, localhost only
|
||||
a.activateBackup(ctx) // runs in goroutine for each site
|
||||
if a.Auth.Dev {
|
||||
go a.devAuth.Run() // dev oauth2 server on :8084
|
||||
}
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
@@ -30,6 +27,7 @@ type admin struct {
|
||||
authenticator auth.Authenticator
|
||||
readOnlyAge int
|
||||
avatarProxy *proxy.Avatar
|
||||
migrator *Migrator
|
||||
}
|
||||
|
||||
func (a *admin) routes(middlewares ...func(http.Handler) http.Handler) chi.Router {
|
||||
@@ -41,10 +39,12 @@ func (a *admin) routes(middlewares ...func(http.Handler) http.Handler) chi.Route
|
||||
router.Get("/user/{userid}", a.getUserInfoCtrl)
|
||||
router.Get("/deleteme", a.deleteMeRequestCtrl)
|
||||
router.Put("/verify/{userid}", a.setVerifyCtrl)
|
||||
router.Get("/export", a.exportCtrl)
|
||||
router.Put("/pin/{id}", a.setPinCtrl)
|
||||
router.Get("/blocked", a.blockedUsersCtrl)
|
||||
router.Put("/readonly", a.setReadOnlyCtrl)
|
||||
|
||||
a.migrator.withRoutes(router) // set migrator routes, i.e. /export and /import
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
@@ -221,31 +221,6 @@ func (a *admin) setPinCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
render.JSON(w, r, JSON{"id": commentID, "locator": locator, "pin": pinStatus})
|
||||
}
|
||||
|
||||
// GET /export?site=site-id?mode=file|stream
|
||||
// exports all comments for siteID as json stream or gz file
|
||||
func (a *admin) exportCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
siteID := r.URL.Query().Get("site")
|
||||
var writer io.Writer = w
|
||||
if r.URL.Query().Get("mode") == "file" {
|
||||
exportFile := fmt.Sprintf("%s-%s.json.gz", siteID, time.Now().Format("20060102"))
|
||||
w.Header().Set("Content-Type", "application/gzip")
|
||||
w.Header().Set("Content-Disposition", "attachment;filename="+exportFile)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
gzWriter := gzip.NewWriter(w)
|
||||
defer func() {
|
||||
if e := gzWriter.Close(); e != nil {
|
||||
log.Printf("[WARN] can't close gzip writer, %s", e)
|
||||
}
|
||||
}()
|
||||
writer = gzWriter
|
||||
}
|
||||
|
||||
if _, err := a.exporter.Export(writer, siteID); err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "export failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (a *admin) checkBlocked(siteID string, user store.User) bool {
|
||||
return a.dataService.IsBlocked(siteID, user.ID)
|
||||
}
|
||||
|
||||
@@ -413,7 +413,6 @@ func TestAdmin_Verify(t *testing.T) {
|
||||
assert.Equal(t, 2, len(comments.Comments), "should have 2 comments")
|
||||
assert.Equal(t, "test test #1", comments.Comments[0].Text)
|
||||
assert.False(t, comments.Comments[0].User.Verified)
|
||||
|
||||
}
|
||||
|
||||
func TestAdmin_ExportStream(t *testing.T) {
|
||||
|
||||
@@ -172,7 +172,6 @@ func sanitizeQuery(u string) string {
|
||||
} else {
|
||||
fnPos = stPos + fnPos
|
||||
}
|
||||
log.Print(stPos, fnPos)
|
||||
for i := stPos; i < fnPos; i++ {
|
||||
out[i] = rune('*')
|
||||
}
|
||||
|
||||
@@ -2,18 +2,13 @@ package api
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/didip/tollbooth"
|
||||
"github.com/didip/tollbooth_chi"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/go-chi/render"
|
||||
|
||||
"github.com/umputun/remark/backend/app/migrator"
|
||||
@@ -22,59 +17,19 @@ import (
|
||||
"github.com/umputun/remark/backend/app/store/keys"
|
||||
)
|
||||
|
||||
// Migrator rest runs on unexposed port and available for local requests only
|
||||
// Migrator rest with import and export controllers
|
||||
type Migrator struct {
|
||||
Version string
|
||||
Cache cache.LoadingCache
|
||||
NativeImporter migrator.Importer
|
||||
DisqusImporter migrator.Importer
|
||||
WordPressImporter migrator.Importer
|
||||
NativeExported migrator.Exporter
|
||||
KeyStore keys.Store
|
||||
|
||||
httpServer *http.Server
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
// Run the listener and request's router, activate rest server
|
||||
// this server doesn't have any authentication and SHOULDN'T BE EXPOSED in any way
|
||||
func (m *Migrator) Run(port int) {
|
||||
log.Printf("[INFO] activate import server on port %d", port)
|
||||
router := m.routes()
|
||||
|
||||
m.lock.Lock()
|
||||
m.httpServer = &http.Server{Addr: fmt.Sprintf("127.0.0.1:%d", port), Handler: router}
|
||||
m.lock.Unlock()
|
||||
|
||||
err := m.httpServer.ListenAndServe()
|
||||
log.Printf("[WARN] http server terminated, %s", err)
|
||||
}
|
||||
|
||||
// Shutdown import http server
|
||||
func (m *Migrator) Shutdown() {
|
||||
log.Print("[WARN] shutdown import server")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
|
||||
m.lock.Lock()
|
||||
if m.httpServer != nil {
|
||||
if err := m.httpServer.Shutdown(ctx); err != nil {
|
||||
log.Printf("[DEBUG] importer shutdown error, %s", err)
|
||||
}
|
||||
}
|
||||
m.lock.Unlock()
|
||||
|
||||
log.Print("[DEBUG] shutdown import server completed")
|
||||
}
|
||||
|
||||
func (m *Migrator) routes() chi.Router {
|
||||
router := chi.NewRouter()
|
||||
router.Use(middleware.RealIP, Recoverer)
|
||||
router.Use(middleware.Throttle(1000), middleware.Timeout(15*time.Minute))
|
||||
router.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil)))
|
||||
router.Use(AppInfo("remark42-migrator", m.Version), Ping, Logger(nil, LogAll))
|
||||
router.Post("/api/v1/admin/import", m.importCtrl)
|
||||
router.Get("/api/v1/admin/export", m.exportCtrl)
|
||||
func (m *Migrator) withRoutes(router chi.Router) chi.Router {
|
||||
router.Get("/export", m.exportCtrl)
|
||||
router.Post("/import", m.importCtrl)
|
||||
return router
|
||||
}
|
||||
|
||||
@@ -83,20 +38,6 @@ func (m *Migrator) routes() chi.Router {
|
||||
func (m *Migrator) importCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
siteID := r.URL.Query().Get("site")
|
||||
secret := r.URL.Query().Get("secret")
|
||||
|
||||
skey, err := m.KeyStore.Get(siteID)
|
||||
if err != nil {
|
||||
render.Status(r, http.StatusForbidden)
|
||||
render.JSON(w, r, JSON{"status": "error", "details": "secret key store"})
|
||||
return
|
||||
}
|
||||
|
||||
if strings.TrimSpace(secret) == "" || secret != skey {
|
||||
render.Status(r, http.StatusForbidden)
|
||||
render.JSON(w, r, JSON{"status": "error", "details": "secret key"})
|
||||
return
|
||||
}
|
||||
|
||||
var importer migrator.Importer
|
||||
switch r.URL.Query().Get("provider") {
|
||||
@@ -108,7 +49,7 @@ func (m *Migrator) importCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
importer = m.NativeImporter
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] import request for site=%s", siteID)
|
||||
log.Printf("[DEBUG] import request for site=%s, provider=%s", siteID, r.URL.Query().Get("provider"))
|
||||
size, err := importer.Import(r.Body, siteID)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "import failed")
|
||||
@@ -120,40 +61,28 @@ func (m *Migrator) importCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
render.JSON(w, r, JSON{"status": "ok", "size": size})
|
||||
}
|
||||
|
||||
// GET /export?site=site-id&secret=12345
|
||||
// GET /export?site=site-id&secret=12345&?mode=file|stream
|
||||
// exports all comments for siteID as gz file
|
||||
func (m *Migrator) exportCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
siteID := r.URL.Query().Get("site")
|
||||
secret := r.URL.Query().Get("secret")
|
||||
|
||||
skey, err := m.KeyStore.Get(siteID)
|
||||
if err != nil {
|
||||
render.Status(r, http.StatusForbidden)
|
||||
render.JSON(w, r, JSON{"status": "error", "details": "secret key store"})
|
||||
return
|
||||
var writer io.Writer = w
|
||||
if r.URL.Query().Get("mode") == "file" {
|
||||
exportFile := fmt.Sprintf("%s-%s.json.gz", siteID, time.Now().Format("20060102"))
|
||||
w.Header().Set("Content-Type", "application/gzip")
|
||||
w.Header().Set("Content-Disposition", "attachment;filename="+exportFile)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
gzWriter := gzip.NewWriter(w)
|
||||
defer func() {
|
||||
if e := gzWriter.Close(); e != nil {
|
||||
log.Printf("[WARN] can't close gzip writer, %s", e)
|
||||
}
|
||||
}()
|
||||
writer = gzWriter
|
||||
}
|
||||
|
||||
if strings.TrimSpace(secret) == "" || secret != skey {
|
||||
render.Status(r, http.StatusForbidden)
|
||||
render.JSON(w, r, JSON{"status": "error", "details": "secret key"})
|
||||
return
|
||||
}
|
||||
|
||||
exportFile := fmt.Sprintf("%s-%s.json.gz", siteID, time.Now().Format("20060102"))
|
||||
log.Printf("[DEBUG] import request for site=%s to %s", siteID, exportFile)
|
||||
|
||||
w.Header().Set("Content-Type", "application/gzip")
|
||||
w.Header().Set("Content-Disposition", "attachment;filename="+exportFile)
|
||||
gzWriter := gzip.NewWriter(w)
|
||||
defer func() {
|
||||
if e := gzWriter.Close(); e != nil {
|
||||
log.Printf("[WARN] can't close gzip writer, %s", e)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := m.NativeExported.Export(gzWriter, siteID); err != nil {
|
||||
log.Printf("[WARN] can't export, %+v", err)
|
||||
if _, err := m.NativeExported.Export(writer, siteID); err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "export failed")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -12,12 +12,15 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/coreos/bbolt"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/umputun/remark/backend/app/migrator"
|
||||
"github.com/umputun/remark/backend/app/rest/auth"
|
||||
"github.com/umputun/remark/backend/app/rest/cache"
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
adminstore "github.com/umputun/remark/backend/app/store/admin"
|
||||
"github.com/umputun/remark/backend/app/store/engine"
|
||||
"github.com/umputun/remark/backend/app/store/keys"
|
||||
"github.com/umputun/remark/backend/app/store/service"
|
||||
@@ -32,7 +35,7 @@ func TestMigrator_Import(t *testing.T) {
|
||||
{"id":"83fd97fd-ff64-48d1-9fb7-ca7769c77037","pid":"p1","text":"<p>test test #2</p>","user":{"name":"developer one","id":"dev","picture":"/api/v1/avatar/remark.image","profile":"https://remark42.com","admin":true,"ip":"ae12fe3b5f129b5cc4cdd2b136b7b7947c4d2741"},"locator":{"site":"radio-t","url":"https://radio-t.com/blah2"},"score":0,"votes":{},"time":"2018-04-30T01:37:00.861387771-05:00"}`)
|
||||
|
||||
client := &http.Client{Timeout: 1 * time.Second}
|
||||
req, err := http.NewRequest("POST", ts.URL+"/api/v1/admin/import?site=radio-t&provider=native&secret=123456", r)
|
||||
req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native&secret=123456", r)
|
||||
assert.Nil(t, err)
|
||||
resp, err := client.Do(req)
|
||||
assert.Nil(t, err)
|
||||
@@ -51,7 +54,7 @@ func TestMigrator_ImportFromWP(t *testing.T) {
|
||||
r := strings.NewReader(strings.Replace(xmlTestWP, "'", "`", -1))
|
||||
|
||||
client := &http.Client{Timeout: 1 * time.Second}
|
||||
req, err := http.NewRequest("POST", ts.URL+"/api/v1/admin/import?site=radio-t&provider=wordpress&secret=123456", r)
|
||||
req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=wordpress&secret=123456", r)
|
||||
assert.Nil(t, err)
|
||||
req.Header.Add("Content-Type", "application/xml; charset=utf-8")
|
||||
resp, err := client.Do(req)
|
||||
@@ -88,11 +91,11 @@ func TestMigrator_ImportRejected(t *testing.T) {
|
||||
{"id":"83fd97fd-ff64-48d1-9fb7-ca7769c77037","pid":"p1","text":"<p>test test #2</p>","user":{"name":"developer one","id":"dev","picture":"/api/v1/avatar/remark.image","profile":"https://remark42.com","admin":true,"ip":"ae12fe3b5f129b5cc4cdd2b136b7b7947c4d2741"},"locator":{"site":"radio-t","url":"https://radio-t.com/blah2"},"score":0,"votes":{},"time":"2018-04-30T01:37:00.861387771-05:00"}`)
|
||||
|
||||
client := &http.Client{Timeout: 1 * time.Second}
|
||||
req, err := http.NewRequest("POST", ts.URL+"/api/v1/admin/import?site=radio-t&provider=native&secret=XYZ", r)
|
||||
req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native&secret=XYZ", r)
|
||||
assert.Nil(t, err)
|
||||
resp, err := client.Do(req)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestMigrator_Export(t *testing.T) {
|
||||
@@ -104,13 +107,14 @@ func TestMigrator_Export(t *testing.T) {
|
||||
{"id":"83fd97fd-ff64-48d1-9fb7-ca7769c77037","pid":"p1","text":"<p>test test #2</p>","user":{"name":"developer one","id":"dev","picture":"/api/v1/avatar/remark.image","profile":"https://remark42.com","admin":true,"ip":"ae12fe3b5f129b5cc4cdd2b136b7b7947c4d2741"},"locator":{"site":"radio-t","url":"https://radio-t.com/blah2"},"score":0,"votes":{},"time":"2018-04-30T01:37:00.861387771-05:00"}`)
|
||||
|
||||
client := &http.Client{Timeout: 1 * time.Second}
|
||||
req, err := http.NewRequest("POST", ts.URL+"/api/v1/admin/import?site=radio-t&provider=native&secret=123456", r)
|
||||
req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native&secret=123456", r)
|
||||
require.Nil(t, err)
|
||||
resp, err := client.Do(req)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, http.StatusCreated, resp.StatusCode)
|
||||
|
||||
req, err = http.NewRequest("GET", ts.URL+"/api/v1/admin/export?site=radio-t&secret=123456", nil)
|
||||
// check file mode
|
||||
req, err = http.NewRequest("GET", ts.URL+"/export?mode=file&site=radio-t&secret=123456", nil)
|
||||
require.Nil(t, err)
|
||||
resp, err = client.Do(req)
|
||||
require.Nil(t, err)
|
||||
@@ -125,44 +129,54 @@ func TestMigrator_Export(t *testing.T) {
|
||||
assert.Equal(t, 2, strings.Count(string(ungzBody), "\"text\""))
|
||||
t.Logf("%s", string(ungzBody))
|
||||
|
||||
req, err = http.NewRequest("GET", ts.URL+"/api/v1/admin/export?site=radio-t&secret=bad", nil)
|
||||
// check stream mode
|
||||
req, err = http.NewRequest("GET", ts.URL+"/export?mode=stream&site=radio-t&secret=123456", nil)
|
||||
require.Nil(t, err)
|
||||
resp, err = client.Do(req)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, 403, resp.StatusCode)
|
||||
}
|
||||
require.Equal(t, 200, resp.StatusCode)
|
||||
require.Equal(t, "text/plain; charset=utf-8", resp.Header.Get("Content-Type"))
|
||||
|
||||
func TestMigrator_Shutdown(t *testing.T) {
|
||||
srv := Migrator{}
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
srv.Shutdown()
|
||||
}()
|
||||
st := time.Now()
|
||||
srv.Run(0)
|
||||
assert.True(t, time.Since(st).Seconds() < 1, "should take about 100ms")
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, strings.Count(string(body), "\n"))
|
||||
assert.Equal(t, 2, strings.Count(string(body), "\"text\""))
|
||||
t.Logf("%s", string(body))
|
||||
|
||||
req, err = http.NewRequest("GET", ts.URL+"/export?site=radio-t&secret=bad", nil)
|
||||
require.Nil(t, err)
|
||||
resp, err = client.Do(req)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
||||
}
|
||||
|
||||
func prepImportSrv(t *testing.T) (svc *Migrator, ds *service.DataStore, ts *httptest.Server) {
|
||||
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: testDb, SiteID: "radio-t"})
|
||||
require.Nil(t, err)
|
||||
dataStore := &service.DataStore{Interface: b, KeyStore: keys.NewStaticStore("123456")}
|
||||
ks := keys.NewStaticStore("123456")
|
||||
dataStore := &service.DataStore{Interface: b, KeyStore: ks}
|
||||
svc = &Migrator{
|
||||
DisqusImporter: &migrator.Disqus{DataStore: dataStore},
|
||||
WordPressImporter: &migrator.WordPress{DataStore: dataStore},
|
||||
NativeImporter: &migrator.Remark{DataStore: dataStore},
|
||||
NativeExported: &migrator.Remark{DataStore: dataStore},
|
||||
Cache: &cache.Nop{},
|
||||
KeyStore: keys.NewStaticStore("123456"),
|
||||
KeyStore: ks,
|
||||
}
|
||||
routes := svc.routes()
|
||||
a := auth.Authenticator{
|
||||
DevPasswd: "password",
|
||||
Providers: nil,
|
||||
AdminStore: adminstore.NewStaticStore([]string{"a1", "a2"}, "admin@remark-42.com"),
|
||||
JWTService: auth.NewJWT(keys.NewStaticStore("123456"), false, time.Minute, time.Hour),
|
||||
KeyStore: ks,
|
||||
}
|
||||
routes := svc.withRoutes(chi.NewRouter().With(a.Auth(true)).With(a.AdminOnly))
|
||||
ts = httptest.NewServer(routes)
|
||||
return svc, dataStore, ts
|
||||
}
|
||||
|
||||
func cleanupImportSrv(m *Migrator, ts *httptest.Server) {
|
||||
ts.Close()
|
||||
m.Shutdown()
|
||||
os.Remove(testDb)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rakyll/statik/fs"
|
||||
|
||||
"github.com/umputun/remark/backend/app/migrator"
|
||||
"github.com/umputun/remark/backend/app/rest"
|
||||
"github.com/umputun/remark/backend/app/rest/auth"
|
||||
"github.com/umputun/remark/backend/app/rest/cache"
|
||||
@@ -37,11 +36,11 @@ type Rest struct {
|
||||
|
||||
DataService *service.DataStore
|
||||
Authenticator auth.Authenticator
|
||||
Exporter migrator.Exporter
|
||||
Cache cache.LoadingCache
|
||||
AvatarProxy *proxy.Avatar
|
||||
ImageProxy *proxy.Image
|
||||
CommentFormatter *store.CommentFormatter
|
||||
Migrator *Migrator
|
||||
|
||||
WebRoot string
|
||||
RemarkURL string
|
||||
@@ -115,7 +114,7 @@ func (s *Rest) routes() chi.Router {
|
||||
|
||||
s.adminService = admin{
|
||||
dataService: s.DataService,
|
||||
exporter: s.Exporter,
|
||||
migrator: s.Migrator,
|
||||
cache: s.Cache,
|
||||
authenticator: s.Authenticator,
|
||||
readOnlyAge: s.ReadOnlyAge,
|
||||
@@ -187,7 +186,7 @@ func (s *Rest) routes() chi.Router {
|
||||
rauth.Post("/deleteme", s.deleteMeCtrl)
|
||||
|
||||
// admin routes, admin users only
|
||||
rauth.Mount("/admin", s.adminService.routes(s.Authenticator.AdminOnly, Logger(nil, LogAll)))
|
||||
rauth.Mount("/admin", s.adminService.routes(s.Authenticator.AdminOnly))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -109,7 +109,6 @@ func prep(t *testing.T) (srv *Rest, ts *httptest.Server) {
|
||||
AdminStore: adminStore,
|
||||
JWTService: auth.NewJWT(keys.NewStaticStore("123456"), false, time.Minute, time.Hour),
|
||||
},
|
||||
Exporter: &migrator.Remark{DataStore: dataStore},
|
||||
Cache: &cache.Nop{},
|
||||
WebRoot: "/tmp",
|
||||
RemarkURL: "https://demo.remark42.com",
|
||||
@@ -117,6 +116,14 @@ func prep(t *testing.T) (srv *Rest, ts *httptest.Server) {
|
||||
ImageProxy: &proxy.Image{},
|
||||
ReadOnlyAge: 10,
|
||||
CommentFormatter: store.NewCommentFormatter(&proxy.Image{}),
|
||||
Migrator: &Migrator{
|
||||
DisqusImporter: &migrator.Disqus{DataStore: dataStore},
|
||||
WordPressImporter: &migrator.WordPress{DataStore: dataStore},
|
||||
NativeImporter: &migrator.Remark{DataStore: dataStore},
|
||||
NativeExported: &migrator.Remark{DataStore: dataStore},
|
||||
Cache: &cache.Nop{},
|
||||
KeyStore: keys.NewStaticStore("123456"),
|
||||
},
|
||||
}
|
||||
srv.ScoreThresholds.Low, srv.ScoreThresholds.Critical = -5, -10
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/umputun/remark/backend/app/rest"
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
"github.com/umputun/remark/backend/app/store/admin"
|
||||
"github.com/umputun/remark/backend/app/store/keys"
|
||||
)
|
||||
|
||||
// Authenticator is top level auth object providing middlewares
|
||||
@@ -17,6 +18,7 @@ type Authenticator struct {
|
||||
JWTService *JWT
|
||||
Providers []Provider
|
||||
AdminStore admin.Store
|
||||
KeyStore keys.Store
|
||||
DevPasswd string
|
||||
PermissionChecker PermissionChecker
|
||||
}
|
||||
@@ -28,6 +30,13 @@ var devUser = store.User{
|
||||
Admin: true,
|
||||
}
|
||||
|
||||
var adminUser = store.User{
|
||||
ID: "admin",
|
||||
Name: "admin",
|
||||
Picture: "/api/v1/avatar/remark.image",
|
||||
Admin: true,
|
||||
}
|
||||
|
||||
// PermissionChecker defines interface to check user flags
|
||||
type PermissionChecker interface {
|
||||
IsVerified(siteID, userID string) bool
|
||||
@@ -41,9 +50,15 @@ 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) {
|
||||
|
||||
// if secret key matches and site in request return admin
|
||||
if a.checkSecretKey(r) {
|
||||
r = rest.SetUserInfo(r, adminUser)
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if a.basicDevUser(w, r) { // use dev user basic auth if enabled
|
||||
user := devUser
|
||||
r = rest.SetUserInfo(r, user)
|
||||
r = rest.SetUserInfo(r, devUser)
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
@@ -97,6 +112,25 @@ func (a *Authenticator) Auth(reqAuth bool) func(http.Handler) http.Handler {
|
||||
return f
|
||||
}
|
||||
|
||||
func (a *Authenticator) checkSecretKey(r *http.Request) bool {
|
||||
if a.KeyStore == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
siteID := r.URL.Query().Get("site")
|
||||
secret := r.URL.Query().Get("secret")
|
||||
|
||||
skey, err := a.KeyStore.Get(siteID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if strings.TrimSpace(secret) == "" || secret != skey {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// refreshExpiredToken makes new token with passed claims, but only if permission allowed
|
||||
func (a *Authenticator) refreshExpiredToken(w http.ResponseWriter, claims *CustomClaims) (*CustomClaims, error) {
|
||||
if a.PermissionChecker != nil {
|
||||
|
||||
@@ -212,6 +212,24 @@ func TestAdminRequired(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestAuthWithSecret(t *testing.T) {
|
||||
a := Authenticator{DevPasswd: "123456", KeyStore: keys.NewStaticStore("secretkey")}
|
||||
router := chi.NewRouter()
|
||||
router.With(a.Auth(true), a.AdminOnly).Get("/auth", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(201)
|
||||
})
|
||||
server := httptest.NewServer(router)
|
||||
defer server.Close()
|
||||
|
||||
resp, err := http.Get(server.URL + "/auth?secret=secretkey")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 201, resp.StatusCode, "valid auth user with secret, admin")
|
||||
|
||||
resp, err = http.Get(server.URL + "/auth?secret=badsecret")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 401, resp.StatusCode, "invalid auth with bad secret")
|
||||
}
|
||||
|
||||
func withBasicAuth(r *http.Request, username, password string) *http.Request {
|
||||
auth := username + ":" + password
|
||||
r.Header.Add("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth)))
|
||||
|
||||
Reference in New Issue
Block a user