diff --git a/Makefile b/Makefile index 27a983cf..fa3b1872 100644 --- a/Makefile +++ b/Makefile @@ -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 \ No newline at end of file diff --git a/backend/app/cmd/backup.go b/backend/app/cmd/backup.go index 7d3047df..4307e474 100644 --- a/backend/app/cmd/backup.go +++ b/backend/app/cmd/backup.go @@ -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) diff --git a/backend/app/cmd/import.go b/backend/app/cmd/import.go index f0ea5729..5e5b3d42 100644 --- a/backend/app/cmd/import.go +++ b/backend/app/cmd/import.go @@ -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 diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index 249f52de..79c42623 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -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 } diff --git a/backend/app/rest/api/admin.go b/backend/app/rest/api/admin.go index c82caebf..0dd3d5b3 100644 --- a/backend/app/rest/api/admin.go +++ b/backend/app/rest/api/admin.go @@ -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) } diff --git a/backend/app/rest/api/admin_test.go b/backend/app/rest/api/admin_test.go index a5805783..567256d9 100644 --- a/backend/app/rest/api/admin_test.go +++ b/backend/app/rest/api/admin_test.go @@ -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) { diff --git a/backend/app/rest/api/middleware.go b/backend/app/rest/api/middleware.go index 15ee1c7d..f73f48ab 100644 --- a/backend/app/rest/api/middleware.go +++ b/backend/app/rest/api/middleware.go @@ -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('*') } diff --git a/backend/app/rest/api/migrator.go b/backend/app/rest/api/migrator.go index 51e5d331..01534e4d 100644 --- a/backend/app/rest/api/migrator.go +++ b/backend/app/rest/api/migrator.go @@ -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 } diff --git a/backend/app/rest/api/migrator_test.go b/backend/app/rest/api/migrator_test.go index 5b62721a..eb0c2b52 100644 --- a/backend/app/rest/api/migrator_test.go +++ b/backend/app/rest/api/migrator_test.go @@ -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":"

test test #2

","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":"

test test #2

","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":"

test test #2

","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) } diff --git a/backend/app/rest/api/rest.go b/backend/app/rest/api/rest.go index 6b2dcc4a..fc3be7ee 100644 --- a/backend/app/rest/api/rest.go +++ b/backend/app/rest/api/rest.go @@ -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)) }) }) diff --git a/backend/app/rest/api/rest_test.go b/backend/app/rest/api/rest_test.go index 8cac32e5..df27dacf 100644 --- a/backend/app/rest/api/rest_test.go +++ b/backend/app/rest/api/rest_test.go @@ -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 diff --git a/backend/app/rest/auth/auth.go b/backend/app/rest/auth/auth.go index e71ce422..5e62b1e6 100644 --- a/backend/app/rest/auth/auth.go +++ b/backend/app/rest/auth/auth.go @@ -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 { diff --git a/backend/app/rest/auth/auth_test.go b/backend/app/rest/auth/auth_test.go index 33dc0327..4efc80f9 100644 --- a/backend/app/rest/auth/auth_test.go +++ b/backend/app/rest/auth/auth_test.go @@ -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)))