diff --git a/goblog.go b/goblog.go index b90341a..2b8d834 100644 --- a/goblog.go +++ b/goblog.go @@ -451,8 +451,8 @@ func main() { // inline script, where the redirect_uri escaping was wrong (#631). router.GET("/login/github", goblog._blog.GithubLogin) router.GET("/wizard", goblog._wizard.SaveToken) - router.POST("/wizard_db", updateDB) - router.POST("/test_db", testDB) + router.POST("/wizard_db", goblog.installOnly, updateDB) + router.POST("/test_db", goblog.installOnly, testDB) router.POST("/api/v1/upload", goblog._admin.UploadFile) router.POST("/api/v1/preview", goblog._admin.Preview) router.PATCH("/api/v1/settings", goblog._admin.UpdateSettings) @@ -641,6 +641,18 @@ func requireJSON() gin.HandlerFunc { } } +// installOnly refuses the database wizard's endpoints once the site is +// installed, that is, once it has a database and an admin. They take no +// credentials because during an install there is nobody to hold any, so +// without this they stay open for good: /wizard_db rewrites .env with +// whatever database the request names, and /test_db opens any file or host +// it is given. +func (g *goblog) installOnly(c *gin.Context) { + if !g._wizard.IsDbNil() && !g._auth.IsWizardMode(c) { + c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "This site is already installed"}) + } +} + // parse the form which should have passed the db info // and then create the env file with it func updateDB(c *gin.Context) { diff --git a/wizard_install_only_test.go b/wizard_install_only_test.go new file mode 100644 index 0000000..634c3f2 --- /dev/null +++ b/wizard_install_only_test.go @@ -0,0 +1,96 @@ +package main + +import ( + "net/http" + "net/http/httptest" + "os" + "strings" + "testing" + + "goblog/auth" + "goblog/tools" + "goblog/wizard" + + "github.com/gin-gonic/gin" + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +// TestInstallOnly: the database wizard's endpoints are for installing. On a +// site that has a database and an admin, a request to them must change +// nothing; before that (no database yet, or a database with no admin) the +// wizard still has to work. +func TestInstallOnly(t *testing.T) { + gin.SetMode(gin.TestMode) + post := func(g *goblog, path string) *httptest.ResponseRecorder { + router := gin.New() + router.SetHTMLTemplate(templateWithErrors(t)) + router.POST("/wizard_db", g.installOnly, updateDB) + router.POST("/test_db", g.installOnly, testDB) + req := httptest.NewRequest(http.MethodPost, path, strings.NewReader("dbtype=sqlite&sqlite_file=other.db")) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + return w + } + app := func(db *gorm.DB) *goblog { + a := auth.New(db, "test") + wz := wizard.New(db, "test") + return &goblog{_auth: &a, _wizard: &wz} + } + openDB := func() *gorm.DB { + db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true}) + if err != nil { + t.Fatal(err) + } + if err := tools.Migrate(db); err != nil { + t.Fatal(err) + } + return db + } + + t.Run("installed site", func(t *testing.T) { + t.Chdir(t.TempDir()) + const env = "database=sqlite\nsqlite_db=live.db\nclient_id=x\nclient_secret=y\n" + if err := os.WriteFile(".env", []byte(env), 0600); err != nil { + t.Fatal(err) + } + db := openDB() + user := auth.BlogUser{Provider: auth.ProviderGitHub, ProviderID: "1", Login: "admin"} + if err := db.Create(&user).Error; err != nil { + t.Fatal(err) + } + if err := db.Create(&auth.AdminUser{BlogUserID: user.ID}).Error; err != nil { + t.Fatal(err) + } + g := app(db) + for _, path := range []string{"/wizard_db", "/test_db"} { + if w := post(g, path); w.Code != http.StatusForbidden { + t.Errorf("POST %s on an installed site: status %d, want 403", path, w.Code) + } + } + if got, _ := os.ReadFile(".env"); string(got) != env { + t.Errorf(".env was rewritten on an installed site:\n%s", got) + } + if _, err := os.Stat("other.db"); err == nil { + t.Error("/test_db created the file it was asked to open") + } + }) + + t.Run("no database yet", func(t *testing.T) { + t.Chdir(t.TempDir()) + if w := post(app(nil), "/wizard_db"); w.Code != http.StatusSeeOther { + t.Errorf("POST /wizard_db before install: status %d, want 303", w.Code) + } + if got, _ := os.ReadFile(".env"); !strings.Contains(string(got), "sqlite_db=other.db") { + t.Errorf(".env not written by the wizard: %q", got) + } + }) + + t.Run("database but no admin yet", func(t *testing.T) { + t.Chdir(t.TempDir()) + if w := post(app(openDB()), "/wizard_db"); w.Code != http.StatusSeeOther { + t.Errorf("POST /wizard_db mid-install: status %d, want 303", w.Code) + } + }) +}