From 4a28f25347addf05708cdff37ecace4139f01779 Mon Sep 17 00:00:00 2001 From: Luke Granger-Brown Date: Thu, 18 Jun 2020 02:52:33 +0100 Subject: Add support for read-only mode for web UI. Fixes #402. --- commands/webui.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'commands') diff --git a/commands/webui.go b/commands/webui.go index 5d3d4b4a..7a0fb2cd 100644 --- a/commands/webui.go +++ b/commands/webui.go @@ -19,15 +19,17 @@ import ( "github.com/spf13/cobra" "github.com/MichaelMure/git-bug/graphql" + "github.com/MichaelMure/git-bug/graphql/config" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/git" "github.com/MichaelMure/git-bug/webui" ) var ( - webUIPort int - webUIOpen bool - webUINoOpen bool + webUIPort int + webUIOpen bool + webUINoOpen bool + webUIReadOnly bool ) const webUIOpenConfigKey = "git-bug.webui.open" @@ -46,7 +48,7 @@ func runWebUI(cmd *cobra.Command, args []string) error { router := mux.NewRouter() - graphqlHandler, err := graphql.NewHandler(repo) + graphqlHandler, err := graphql.NewHandler(repo, config.Config{ReadOnly: webUIReadOnly}) if err != nil { return err } @@ -261,5 +263,6 @@ func init() { webUICmd.Flags().BoolVar(&webUIOpen, "open", false, "Automatically open the web UI in the default browser") webUICmd.Flags().BoolVar(&webUINoOpen, "no-open", false, "Prevent the automatic opening of the web UI in the default browser") webUICmd.Flags().IntVarP(&webUIPort, "port", "p", 0, "Port to listen to (default is random)") + webUICmd.Flags().BoolVar(&webUIReadOnly, "read-only", false, "Whether to run the web UI in read-only mode") } -- cgit From 43c78da02ecd2d49a45632e5b3270cd0c9da474f Mon Sep 17 00:00:00 2001 From: Luke Granger-Brown Date: Thu, 18 Jun 2020 03:11:24 +0100 Subject: Don't permit file uploads in read-only mode --- commands/webui.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'commands') diff --git a/commands/webui.go b/commands/webui.go index 7a0fb2cd..87ccf5d4 100644 --- a/commands/webui.go +++ b/commands/webui.go @@ -62,7 +62,9 @@ func runWebUI(cmd *cobra.Command, args []string) error { router.Path("/playground").Handler(playground.Handler("git-bug", "/graphql")) router.Path("/graphql").Handler(graphqlHandler) router.Path("/gitfile/{hash}").Handler(newGitFileHandler(repo)) - router.Path("/upload").Methods("POST").Handler(newGitUploadFileHandler(repo)) + if !webUIReadOnly { + router.Path("/upload").Methods("POST").Handler(newGitUploadFileHandler(repo)) + } router.PathPrefix("/").Handler(http.FileServer(assetsHandler)) srv := &http.Server{ -- cgit From dfdb5e090b9368d336cb7552c8c462bef243fbb0 Mon Sep 17 00:00:00 2001 From: Luke Granger-Brown Date: Thu, 18 Jun 2020 03:11:38 +0100 Subject: Verify that we have an identity only in read-write mode --- commands/webui.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'commands') diff --git a/commands/webui.go b/commands/webui.go index 87ccf5d4..e1f592df 100644 --- a/commands/webui.go +++ b/commands/webui.go @@ -20,6 +20,7 @@ import ( "github.com/MichaelMure/git-bug/graphql" "github.com/MichaelMure/git-bug/graphql/config" + "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/git" "github.com/MichaelMure/git-bug/webui" @@ -43,6 +44,13 @@ func runWebUI(cmd *cobra.Command, args []string) error { } } + if !webUIReadOnly { + // Verify that we have an identity. + if _, err := identity.GetUserIdentity(repo); err != nil { + return err + } + } + addr := fmt.Sprintf("127.0.0.1:%d", webUIPort) webUiAddr := fmt.Sprintf("http://%s", addr) @@ -253,7 +261,7 @@ var webUICmd = &cobra.Command{ Available git config: git-bug.webui.open [bool]: control the automatic opening of the web UI in the default browser `, - PreRunE: loadRepoEnsureUser, + PreRunE: loadRepo, RunE: runWebUI, } -- cgit From 766aff2b2f9db339d7c42321fe6cd37309631be3 Mon Sep 17 00:00:00 2001 From: Luke Granger-Brown Date: Thu, 18 Jun 2020 19:31:28 +0100 Subject: Change graphql Go handlers to pluck identity out of context instead. --- commands/webui.go | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) (limited to 'commands') diff --git a/commands/webui.go b/commands/webui.go index e1f592df..24bdeced 100644 --- a/commands/webui.go +++ b/commands/webui.go @@ -19,7 +19,6 @@ import ( "github.com/spf13/cobra" "github.com/MichaelMure/git-bug/graphql" - "github.com/MichaelMure/git-bug/graphql/config" "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/git" @@ -35,6 +34,15 @@ var ( const webUIOpenConfigKey = "git-bug.webui.open" +func authMiddleware(repo repository.RepoCommon, id *identity.Identity) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := identity.AttachToContext(r.Context(), repo, id) + next.ServeHTTP(w, r.WithContext(ctx)) + }) + } +} + func runWebUI(cmd *cobra.Command, args []string) error { if webUIPort == 0 { var err error @@ -44,9 +52,12 @@ func runWebUI(cmd *cobra.Command, args []string) error { } } + var id *identity.Identity if !webUIReadOnly { // Verify that we have an identity. - if _, err := identity.GetUserIdentity(repo); err != nil { + var err error + id, err = identity.GetUserIdentity(repo) + if err != nil { return err } } @@ -56,7 +67,7 @@ func runWebUI(cmd *cobra.Command, args []string) error { router := mux.NewRouter() - graphqlHandler, err := graphql.NewHandler(repo, config.Config{ReadOnly: webUIReadOnly}) + graphqlHandler, err := graphql.NewHandler(repo) if err != nil { return err } @@ -70,10 +81,9 @@ func runWebUI(cmd *cobra.Command, args []string) error { router.Path("/playground").Handler(playground.Handler("git-bug", "/graphql")) router.Path("/graphql").Handler(graphqlHandler) router.Path("/gitfile/{hash}").Handler(newGitFileHandler(repo)) - if !webUIReadOnly { - router.Path("/upload").Methods("POST").Handler(newGitUploadFileHandler(repo)) - } + router.Path("/upload").Methods("POST").Handler(newGitUploadFileHandler(repo)) router.PathPrefix("/").Handler(http.FileServer(assetsHandler)) + router.Use(authMiddleware(repo, id)) srv := &http.Server{ Addr: addr, @@ -200,6 +210,11 @@ func newGitUploadFileHandler(repo repository.Repo) http.Handler { } func (gufh *gitUploadFileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + if identity.ForContext(r.Context(), gufh.repo) == nil { + http.Error(rw, fmt.Sprintf("read-only mode or not logged in"), http.StatusForbidden) + return + } + // 100MB (github limit) var maxUploadSize int64 = 100 * 1000 * 1000 r.Body = http.MaxBytesReader(rw, r.Body, maxUploadSize) -- cgit From e5a316e40c377a8563e92f62b0773ed34321a91a Mon Sep 17 00:00:00 2001 From: Luke Granger-Brown Date: Fri, 19 Jun 2020 00:26:47 +0100 Subject: Pull out context-stuff from identity into graphqlidentity package --- commands/webui.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) (limited to 'commands') diff --git a/commands/webui.go b/commands/webui.go index 24bdeced..90679c79 100644 --- a/commands/webui.go +++ b/commands/webui.go @@ -19,6 +19,7 @@ import ( "github.com/spf13/cobra" "github.com/MichaelMure/git-bug/graphql" + "github.com/MichaelMure/git-bug/graphql/graphqlidentity" "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" "github.com/MichaelMure/git-bug/util/git" @@ -34,10 +35,10 @@ var ( const webUIOpenConfigKey = "git-bug.webui.open" -func authMiddleware(repo repository.RepoCommon, id *identity.Identity) func(http.Handler) http.Handler { +func authMiddleware(id *identity.Identity) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := identity.AttachToContext(r.Context(), repo, id) + ctx := graphqlidentity.AttachToContext(r.Context(), id) next.ServeHTTP(w, r.WithContext(ctx)) }) } @@ -83,7 +84,7 @@ func runWebUI(cmd *cobra.Command, args []string) error { router.Path("/gitfile/{hash}").Handler(newGitFileHandler(repo)) router.Path("/upload").Methods("POST").Handler(newGitUploadFileHandler(repo)) router.PathPrefix("/").Handler(http.FileServer(assetsHandler)) - router.Use(authMiddleware(repo, id)) + router.Use(authMiddleware(id)) srv := &http.Server{ Addr: addr, @@ -210,7 +211,11 @@ func newGitUploadFileHandler(repo repository.Repo) http.Handler { } func (gufh *gitUploadFileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { - if identity.ForContext(r.Context(), gufh.repo) == nil { + id, err := graphqlidentity.ForContextUncached(r.Context(), gufh.repo) + if err != nil { + http.Error(rw, fmt.Sprintf("loading identity: %v", err), http.StatusInternalServerError) + return + } else if id == nil { http.Error(rw, fmt.Sprintf("read-only mode or not logged in"), http.StatusForbidden) return } -- cgit From 5f72b04ef8e84b1c367ca6874519706318e351f5 Mon Sep 17 00:00:00 2001 From: Luke Granger-Brown Date: Fri, 19 Jun 2020 00:35:56 +0100 Subject: Use ErrNotAuthenticated --- commands/webui.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) (limited to 'commands') diff --git a/commands/webui.go b/commands/webui.go index 90679c79..c07f74fd 100644 --- a/commands/webui.go +++ b/commands/webui.go @@ -211,13 +211,13 @@ func newGitUploadFileHandler(repo repository.Repo) http.Handler { } func (gufh *gitUploadFileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { - id, err := graphqlidentity.ForContextUncached(r.Context(), gufh.repo) - if err != nil { - http.Error(rw, fmt.Sprintf("loading identity: %v", err), http.StatusInternalServerError) - return - } else if id == nil { + _, err := graphqlidentity.ForContextUncached(r.Context(), gufh.repo) + if err == graphqlidentity.ErrNotAuthenticated { http.Error(rw, fmt.Sprintf("read-only mode or not logged in"), http.StatusForbidden) return + } else if err != nil { + http.Error(rw, fmt.Sprintf("loading identity: %v", err), http.StatusInternalServerError) + return } // 100MB (github limit) -- cgit From 2ab6381a94d55fa22b80acdbb18849d6b24951f9 Mon Sep 17 00:00:00 2001 From: Michael Muré Date: Sun, 21 Jun 2020 22:12:04 +0200 Subject: Reorganize the webUI and API code Included in the changes: - create a new /api root package to hold all API code, migrate /graphql in there - git API handlers all use the cache instead of the repo directly - git API handlers are now tested - git API handlers now require a "repo" mux parameter - lots of untangling of API/handlers/middleware - less code in commands/webui.go --- commands/webui.go | 176 +++++++----------------------------------------------- 1 file changed, 20 insertions(+), 156 deletions(-) (limited to 'commands') diff --git a/commands/webui.go b/commands/webui.go index c07f74fd..83480e08 100644 --- a/commands/webui.go +++ b/commands/webui.go @@ -1,11 +1,8 @@ package commands import ( - "bytes" "context" - "encoding/json" "fmt" - "io/ioutil" "log" "net/http" "os" @@ -18,11 +15,12 @@ import ( "github.com/skratchdot/open-golang/open" "github.com/spf13/cobra" - "github.com/MichaelMure/git-bug/graphql" - "github.com/MichaelMure/git-bug/graphql/graphqlidentity" + "github.com/MichaelMure/git-bug/api/auth" + "github.com/MichaelMure/git-bug/api/graphql" + httpapi "github.com/MichaelMure/git-bug/api/http" + "github.com/MichaelMure/git-bug/cache" "github.com/MichaelMure/git-bug/identity" "github.com/MichaelMure/git-bug/repository" - "github.com/MichaelMure/git-bug/util/git" "github.com/MichaelMure/git-bug/webui" ) @@ -35,15 +33,6 @@ var ( const webUIOpenConfigKey = "git-bug.webui.open" -func authMiddleware(id *identity.Identity) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := graphqlidentity.AttachToContext(r.Context(), id) - next.ServeHTTP(w, r.WithContext(ctx)) - }) - } -} - func runWebUI(cmd *cobra.Command, args []string) error { if webUIPort == 0 { var err error @@ -53,38 +42,36 @@ func runWebUI(cmd *cobra.Command, args []string) error { } } - var id *identity.Identity + addr := fmt.Sprintf("127.0.0.1:%d", webUIPort) + webUiAddr := fmt.Sprintf("http://%s", addr) + + router := mux.NewRouter() + + // If the webUI is not read-only, use an authentication middleware with a + // fixed identity: the default user of the repo + // TODO: support dynamic authentication with OAuth if !webUIReadOnly { - // Verify that we have an identity. - var err error - id, err = identity.GetUserIdentity(repo) + author, err := identity.GetUserIdentity(repo) if err != nil { return err } + router.Use(auth.Middleware(author.Id())) } - addr := fmt.Sprintf("127.0.0.1:%d", webUIPort) - webUiAddr := fmt.Sprintf("http://%s", addr) - - router := mux.NewRouter() - - graphqlHandler, err := graphql.NewHandler(repo) + mrc := cache.NewMultiRepoCache() + _, err := mrc.RegisterDefaultRepository(repo) if err != nil { return err } - assetsHandler := &fileSystemWithDefault{ - FileSystem: webui.WebUIAssets, - defaultFile: "index.html", - } + graphqlHandler := graphql.NewHandler(mrc) // Routes router.Path("/playground").Handler(playground.Handler("git-bug", "/graphql")) router.Path("/graphql").Handler(graphqlHandler) - router.Path("/gitfile/{hash}").Handler(newGitFileHandler(repo)) - router.Path("/upload").Methods("POST").Handler(newGitUploadFileHandler(repo)) - router.PathPrefix("/").Handler(http.FileServer(assetsHandler)) - router.Use(authMiddleware(id)) + router.Path("/gitfile/{repo}/{hash}").Handler(httpapi.NewGitFileHandler(mrc)) + router.Path("/upload/{repo}").Methods("POST").Handler(httpapi.NewGitUploadFileHandler(mrc)) + router.PathPrefix("/").Handler(webui.NewHandler()) srv := &http.Server{ Addr: addr, @@ -151,128 +138,6 @@ func runWebUI(cmd *cobra.Command, args []string) error { return nil } -// implement a http.FileSystem that will serve a default file when the looked up -// file doesn't exist. Useful for Single-Page App that implement routing client -// side, where the server has to return the root index.html file for every route. -type fileSystemWithDefault struct { - http.FileSystem - defaultFile string -} - -func (fswd *fileSystemWithDefault) Open(name string) (http.File, error) { - f, err := fswd.FileSystem.Open(name) - if os.IsNotExist(err) { - return fswd.FileSystem.Open(fswd.defaultFile) - } - return f, err -} - -// implement a http.Handler that will read and server git blob. -type gitFileHandler struct { - repo repository.Repo -} - -func newGitFileHandler(repo repository.Repo) http.Handler { - return &gitFileHandler{ - repo: repo, - } -} - -func (gfh *gitFileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { - hash := git.Hash(mux.Vars(r)["hash"]) - - if !hash.IsValid() { - http.Error(rw, "invalid git hash", http.StatusBadRequest) - return - } - - // TODO: this mean that the whole file will he buffered in memory - // This can be a problem for big files. There might be a way around - // that by implementing a io.ReadSeeker that would read and discard - // data when a seek is called. - data, err := gfh.repo.ReadData(git.Hash(hash)) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - http.ServeContent(rw, r, "", time.Now(), bytes.NewReader(data)) -} - -// implement a http.Handler that will accept and store content into git blob. -type gitUploadFileHandler struct { - repo repository.Repo -} - -func newGitUploadFileHandler(repo repository.Repo) http.Handler { - return &gitUploadFileHandler{ - repo: repo, - } -} - -func (gufh *gitUploadFileHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { - _, err := graphqlidentity.ForContextUncached(r.Context(), gufh.repo) - if err == graphqlidentity.ErrNotAuthenticated { - http.Error(rw, fmt.Sprintf("read-only mode or not logged in"), http.StatusForbidden) - return - } else if err != nil { - http.Error(rw, fmt.Sprintf("loading identity: %v", err), http.StatusInternalServerError) - return - } - - // 100MB (github limit) - var maxUploadSize int64 = 100 * 1000 * 1000 - r.Body = http.MaxBytesReader(rw, r.Body, maxUploadSize) - if err := r.ParseMultipartForm(maxUploadSize); err != nil { - http.Error(rw, "file too big (100MB max)", http.StatusBadRequest) - return - } - - file, _, err := r.FormFile("uploadfile") - if err != nil { - http.Error(rw, "invalid file", http.StatusBadRequest) - return - } - defer file.Close() - fileBytes, err := ioutil.ReadAll(file) - if err != nil { - http.Error(rw, "invalid file", http.StatusBadRequest) - return - } - - filetype := http.DetectContentType(fileBytes) - if filetype != "image/jpeg" && filetype != "image/jpg" && - filetype != "image/gif" && filetype != "image/png" { - http.Error(rw, "invalid file type", http.StatusBadRequest) - return - } - - hash, err := gufh.repo.StoreData(fileBytes) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - type response struct { - Hash string `json:"hash"` - } - - resp := response{Hash: string(hash)} - - js, err := json.Marshal(resp) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } - - rw.Header().Set("Content-Type", "application/json") - _, err = rw.Write(js) - if err != nil { - http.Error(rw, err.Error(), http.StatusInternalServerError) - return - } -} - var webUICmd = &cobra.Command{ Use: "webui", Short: "Launch the web UI.", @@ -294,5 +159,4 @@ func init() { webUICmd.Flags().BoolVar(&webUINoOpen, "no-open", false, "Prevent the automatic opening of the web UI in the default browser") webUICmd.Flags().IntVarP(&webUIPort, "port", "p", 0, "Port to listen to (default is random)") webUICmd.Flags().BoolVar(&webUIReadOnly, "read-only", false, "Whether to run the web UI in read-only mode") - } -- cgit