diff --git a/flag.go b/flag.go new file mode 100644 index 0000000..0946412 --- /dev/null +++ b/flag.go @@ -0,0 +1,48 @@ +package main + +import ( + "flag" + "fmt" + "sort" + + "github.com/gorilla/securecookie" +) + +var ( + fs = flag.NewFlagSet("invitea", flag.ExitOnError) + + // Optional + jsonLog = fs.Bool("json", false, "Enable JSON logging") + port = fs.Int("port", 8080, "Port to run on") + sessionSecret = fs.String("session-secret", string(securecookie.GenerateRandomKey(32)), "Session secret") + + // Required + domain = fs.String("domain", "", "Domain Invitea is running on") + giteaURL = fs.String("gitea.url", "", "Gitea URL") + giteaClientKey = fs.String("gitea.client-key", "", "Gitea OAuth2 Client Key") + giteaClientSecret = fs.String("gitea.client-secret", "", "Gitea OAuth2 Client Secret") + giteaToken = fs.String("gitea.token", "", "Gitea admin token") +) + +func requiredFlags() error { + required := map[string]*string{ + "domain": domain, + "gitea.url": giteaURL, + "gitea.client-key": giteaClientKey, + "gitea.client-secret": giteaClientSecret, + "gitea.token": giteaToken, + } + + var unset []string + for k, v := range required { + if *v == "" { + unset = append(unset, k) + } + } + if len(unset) > 0 { + sort.Strings(unset) + return fmt.Errorf("the following flags were unset, but are required: %v", unset) + } + + return nil +} diff --git a/go.mod b/go.mod index 917c488..6305ff6 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,10 @@ module go.jolheiser.com/invitea go 1.17 require ( + code.gitea.io/sdk/gitea v0.15.1 github.com/go-chi/chi/v5 v5.0.7 + github.com/gorilla/securecookie v1.1.1 + github.com/gorilla/sessions v1.1.1 github.com/markbates/goth v1.69.0 github.com/peterbourgon/ff/v3 v3.1.2 github.com/rs/zerolog v1.26.1 @@ -13,8 +16,7 @@ require ( github.com/golang/protobuf v1.4.2 // indirect github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/mux v1.6.2 // indirect - github.com/gorilla/securecookie v1.1.1 // indirect - github.com/gorilla/sessions v1.1.1 // indirect + github.com/hashicorp/go-version v1.2.1 // indirect github.com/pelletier/go-toml v1.6.0 // indirect golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d // indirect golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect diff --git a/go.sum b/go.sum index 4b6829c..44f7999 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,9 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE= +code.gitea.io/sdk/gitea v0.15.1 h1:WJreC7YYuxbn0UDaPuWIe/mtiNKTvLN8MLkaw71yx/M= +code.gitea.io/sdk/gitea v0.15.1/go.mod h1:klY2LVI3s3NChzIk/MzMn7G1FHrfU7qd63iSMVoHRBA= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -114,6 +117,8 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.1.1 h1:YMDmfaK68mUixINzY/XjscuJ47uXFWSSHzFbBQM0PrE= github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w= +github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -309,6 +314,7 @@ golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= diff --git a/main.go b/main.go index 3e7746f..d3d0e90 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - "flag" "fmt" "net/http" "os" @@ -16,9 +15,6 @@ import ( ) func main() { - fs := flag.NewFlagSet("invitea", flag.ExitOnError) - jsonLog := fs.Bool("json", false, "Enable JSON logging") - port := fs.Int("port", 8080, "Port to run on") level := zerolog.InfoLevel fs.Func("level", "Logging level (debug, info, error)", func(s string) error { lvl, err := zerolog.ParseLevel(s) @@ -38,12 +34,22 @@ func main() { } zerolog.SetGlobalLevel(level) - if !*jsonLog { log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) } - r := router.New() + if err := requiredFlags(); err != nil { + log.Fatal().Err(err).Msg("") + } + + r := router.New(router.Config{ + Domain: *domain, + SessionSecret: *sessionSecret, + GiteaURL: *giteaURL, + GiteaClientKey: *giteaClientKey, + GiteaClientSecret: *giteaClientSecret, + GiteaToken: *giteaToken, + }) go func() { log.Debug().Msgf("Listening at http://localhost:%d", *port) diff --git a/router/router.go b/router/router.go index 28ff830..f589cfa 100644 --- a/router/router.go +++ b/router/router.go @@ -1,33 +1,50 @@ package router import ( + "fmt" + "net/http" + "strings" + "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "github.com/gorilla/sessions" "github.com/markbates/goth" "github.com/markbates/goth/gothic" "github.com/markbates/goth/providers/gitea" "github.com/rs/zerolog/log" - "net/http" ) -func New() *chi.Mux { - // TODO pass in domain, strip ending slashes - domain := "https://git.jojodev.com" - clientKey := "18a5e9c3-fc3e-4d78-8564-74ec36531541" - clientSecret := "SZSd2QgciJL4wmPRxrVPnUwtrcGSn5nEI4QRh1aZM3JT" - callbackURL := "http://localhost:8080/auth/callback" +type Config struct { + Domain string + SessionSecret string + + GiteaURL string + GiteaClientKey string + GiteaClientSecret string + GiteaToken string +} + +func New(cfg Config) *chi.Mux { + cfg.GiteaURL = strings.TrimRight(cfg.GiteaURL, "/") + cfg.Domain = strings.TrimRight(cfg.Domain, "/") + callbackURL := fmt.Sprintf("%s/auth/callback", cfg.Domain) goth.UseProviders( - gitea.NewCustomisedURL(clientKey, clientSecret, callbackURL, - domain+"/login/oauth/authorize", - domain+"/login/oauth/access_token", - domain+"/api/v1/user", + gitea.NewCustomisedURL(cfg.GiteaClientKey, cfg.GiteaClientSecret, callbackURL, + fmt.Sprintf("%s/login/oauth/authorize", cfg.GiteaURL), + fmt.Sprintf("%s/login/oauth/access_token", cfg.GiteaURL), + fmt.Sprintf("%s/api/v1/user", cfg.GiteaURL), ), ) + gothStore := sessions.NewCookieStore([]byte(cfg.GiteaClientSecret)) + gothStore.Options.HttpOnly = true + gothic.Store = gothStore gothic.GetProviderName = func(_ *http.Request) (string, error) { return "gitea", nil } + store := NewSessionStore(cfg.SessionSecret, cfg.GiteaURL) + r := chi.NewMux() r.Use(middleware.Logger) r.Use(middleware.Recoverer) @@ -37,26 +54,38 @@ func New() *chi.Mux { NoColor: true, }) + r.Route("/", func(r chi.Router) { + r.Use(store.Middleware) + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(fmt.Sprintf("isAdmin: %t", r.Context().Value("isAdmin").(bool)))) + }) + }) + r.Route("/auth", func(r chi.Router) { r.Get("/login", func(w http.ResponseWriter, r *http.Request) { - _, err := gothic.CompleteUserAuth(w, r) + user, err := gothic.CompleteUserAuth(w, r) if err != nil { gothic.BeginAuthHandler(w, r) return } + + if err := store.Auth(w, r, user.AccessToken); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + http.Redirect(w, r, cfg.Domain, http.StatusFound) }) r.Get("/callback", func(w http.ResponseWriter, r *http.Request) { - _, err := gothic.CompleteUserAuth(w, r) + user, err := gothic.CompleteUserAuth(w, r) if err != nil { http.Error(w, err.Error(), http.StatusUnauthorized) return } - }) - r.Get("/logout", func(w http.ResponseWriter, r *http.Request) { - if err := gothic.Logout(w, r); err != nil { + if err := store.Auth(w, r, user.AccessToken); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) + return } - return + http.Redirect(w, r, cfg.Domain, http.StatusFound) }) }) diff --git a/router/session.go b/router/session.go index 7ef135b..680ffd2 100644 --- a/router/session.go +++ b/router/session.go @@ -1 +1,63 @@ package router + +import ( + "context" + "net/http" + + "code.gitea.io/sdk/gitea" + + "github.com/gorilla/sessions" + "github.com/markbates/goth/gothic" +) + +const sessionCookie = "_invitea_session" + +type SessionStore struct { + Store sessions.Store + GiteaURL string +} + +func (s *SessionStore) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + sess, err := s.Store.Get(r, sessionCookie) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if _, ok := sess.Values["authenticated"]; !ok { + gothic.BeginAuthHandler(w, r) + return + } + + r = r.WithContext(context.WithValue(r.Context(), "isAdmin", sess.Values["isAdmin"])) + next.ServeHTTP(w, r) + }) +} + +func (s *SessionStore) Auth(w http.ResponseWriter, r *http.Request, token string) error { + client, err := gitea.NewClient(s.GiteaURL, gitea.SetToken(token)) + if err != nil { + return err + } + profile, _, err := client.GetMyUserInfo() + if err != nil { + return err + } + + sess, err := s.Store.New(r, sessionCookie) + if err != nil { + return err + } + sess.Values["authenticated"] = true + sess.Values["isAdmin"] = profile.IsAdmin + return s.Store.Save(r, w, sess) +} + +func NewSessionStore(sessionSecret, giteURL string) *SessionStore { + store := sessions.NewCookieStore([]byte(sessionSecret)) + return &SessionStore{ + Store: store, + GiteaURL: giteURL, + } +}