Skip to content

Commit

Permalink
feat: add support for github app installation auth
Browse files Browse the repository at this point in the history
  • Loading branch information
emmanuelgautier committed Mar 5, 2024
1 parent ca56abe commit 57ef91e
Show file tree
Hide file tree
Showing 13 changed files with 186 additions and 69 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,10 @@ go.work
# gitignore file
.env

# Giteway files
dist/
giteway
config.yaml

# Github files
*.private-key.pem
10 changes: 10 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@
"program": "${workspaceRoot}",
"args": ["serve"],
"envFile": "${workspaceRoot}/.env"
},

{
"name": "Launch With Config file",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceRoot}",
"args": ["serve", "--config", "./config.yaml"],
"envFile": "${workspaceRoot}/.env"
}
]
}
5 changes: 3 additions & 2 deletions api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package api

import (
"github.com/gin-gonic/gin"
"github.com/thegalactiks/giteway/github"
"github.com/thegalactiks/giteway/internal/config"
)

Expand All @@ -24,9 +25,9 @@ func NewHandler(cfg *config.Config, e *gin.Engine) *Handler {
return &Handler{cfg, e}
}

func Routes(r *gin.Engine, h *Handler) {
func Routes(r *gin.Engine, h *Handler, githubService *github.GithubService) {
adminApi := r.Group("/")
adminApi.Use(HostingMiddleware(), OwnerMiddleware())
adminApi.Use(OwnerMiddleware(), HostingMiddleware(githubService))
adminApi.GET("/repos/:hosting/:owner", h.GetRepositories)
adminApi.GET("/repos/:hosting/:owner/:repo", RepoMiddleware(), h.GetRepository)

Expand Down
60 changes: 39 additions & 21 deletions api/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ import (
"github.com/thegalactiks/giteway/hosting"
)

const (
GithubHost = "github.com"
GitlabHost = "gitlab.com"
)

func getTokenFromContext(ctx *gin.Context) (*string, error) {
authHeader := ctx.GetHeader("Authorization")
if authHeader == "" {
Expand All @@ -23,27 +28,14 @@ func getTokenFromContext(ctx *gin.Context) (*string, error) {
}

token := authParts[1]

return &token, nil
}

func getHostingFromContext(hosting string, token *string) (hosting.GitHostingService, error) {
switch hosting {
case "github.com":
return github.NewGithubService(token)

case "gitlab.com":
if token == nil || *token == "" {
return nil, errors.New("gitlab require a token")
}

return gitlab.NewGitlabService(*token)
if token == "" {
return nil, errors.New("invalid authorization header")
}

return nil, errors.New("unknown hosting service")
return &token, nil
}

func HostingMiddleware() gin.HandlerFunc {
func HostingMiddleware(githubService *github.GithubService) gin.HandlerFunc {
return func(ctx *gin.Context) {
token, err := getTokenFromContext(ctx)
if err != nil {
Expand All @@ -52,10 +44,36 @@ func HostingMiddleware() gin.HandlerFunc {
return
}

hosting := ctx.Param("hosting")
service, err := getHostingFromContext(hosting, token)
if err != nil {
RespondError(ctx, http.StatusBadRequest, "unknown git provider", err)
hostingParam := ctx.Param("hosting")
ownerParam := ctx.Param("owner")
var service hosting.GitHostingService
if hostingParam == GithubHost {
if token != nil {
service, err = githubService.WithAuthToken(ctx, *token)
if err != nil {
RespondError(ctx, http.StatusUnauthorized, "invalid github token", err)
ctx.Abort()
return
}
} else if ownerParam != "" && githubService.IsKnownInstallation(ownerParam) {
service, err = githubService.WithInstallationOwner(ownerParam)
if err != nil {
RespondError(ctx, http.StatusUnauthorized, "invalid github installation", err)
ctx.Abort()
return
}
} else {
service = githubService
}
} else if hostingParam == GitlabHost {
service, err = gitlab.NewGitlabService(*token)
if err != nil {
RespondError(ctx, http.StatusUnauthorized, "invalid gitlab token", err)
ctx.Abort()
return
}
} else {
RespondError(ctx, http.StatusBadRequest, "unknown git provider")
ctx.Abort()
return
}
Expand Down
7 changes: 1 addition & 6 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,11 @@ import (
"github.com/spf13/cobra"
)

var (
configFile string
)

func NewRootCmd() (cmd *cobra.Command) {
var rootCmd = &cobra.Command{
Use: "Giteway",
}
rootCmd.PersistentFlags().StringVarP(&configFile, "conf", "c", "", "config file path")
rootCmd.AddCommand(serve.NewServeCmd(configFile))
rootCmd.AddCommand(serve.NewServeCmd())

return rootCmd
}
Expand Down
38 changes: 23 additions & 15 deletions cmd/serve/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,16 @@ import (
"go.uber.org/zap/zapcore"

"github.com/thegalactiks/giteway/api"
"github.com/thegalactiks/giteway/github"
"github.com/thegalactiks/giteway/internal/config"
"github.com/thegalactiks/giteway/internal/logging"
"github.com/thegalactiks/giteway/internal/otel"
)

var (
configFile string
)

func timeoutMiddleware(timeoutMS time.Duration) gin.HandlerFunc {
return timeout.New(
timeout.WithTimeout(timeoutMS*time.Millisecond),
Expand All @@ -41,24 +46,24 @@ func timeoutMiddleware(timeoutMS time.Duration) gin.HandlerFunc {
)
}

func NewServeCmd(configFile string) (serveCmd *cobra.Command) {
cfg, err := config.New(configFile)
if err != nil {
log.Fatal(err)
}
logging.SetConfig(&logging.Config{
Encoding: cfg.LoggingConfig.Encoding,
Level: zapcore.Level(cfg.LoggingConfig.Level),
Development: cfg.LoggingConfig.Development,
})
defer logging.DefaultLogger().Sync()

tp := otel.InitTracerProvider()
mp := otel.InitMeterProvider()

func NewServeCmd() (serveCmd *cobra.Command) {
serveCmd = &cobra.Command{
Use: "serve",
Run: func(cmd *cobra.Command, args []string) {
cfg, err := config.New(configFile)
if err != nil {
log.Fatal(err)
}
logging.SetConfig(&logging.Config{
Encoding: cfg.LoggingConfig.Encoding,
Level: zapcore.Level(cfg.LoggingConfig.Level),
Development: cfg.LoggingConfig.Development,
})
defer logging.DefaultLogger().Sync()

tp := otel.InitTracerProvider()
mp := otel.InitMeterProvider()

app := fx.New(
fx.Supply(cfg),
fx.Supply(logging.DefaultLogger().Desugar()),
Expand All @@ -71,6 +76,7 @@ func NewServeCmd(configFile string) (serveCmd *cobra.Command) {
printAppInfo,
),
fx.Provide(
github.NewGithubService,
api.NewHandler,
newHTTPServer,
),
Expand All @@ -83,6 +89,8 @@ func NewServeCmd(configFile string) (serveCmd *cobra.Command) {
},
}

serveCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "config file path")

return serveCmd
}

Expand Down
68 changes: 55 additions & 13 deletions github/github.go
Original file line number Diff line number Diff line change
@@ -1,33 +1,75 @@
package github

import (
"context"
"fmt"
"net/http"
"os"

"github.com/bradleyfalzon/ghinstallation/v2"
"github.com/google/go-github/v60/github"
"github.com/thegalactiks/giteway/hosting"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"github.com/thegalactiks/giteway/internal/config"
internalhttp "github.com/thegalactiks/giteway/internal/http"
)

type GithubService struct {
client *github.Client
hasToken bool
cfg *config.GithubConfig
client *github.Client
user *github.User
}

var _ hosting.GitHostingService = (*GithubService)(nil)

func NewGithubService(token *string) (*GithubService, error) {
hasToken := false
httpClient := &http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
func NewGithubService(cfg *config.Config) *GithubService {
client := github.NewClient(internalhttp.NewHttpClient(nil))
return &GithubService{
cfg: &cfg.GithubConfig,
client: client,
}
}

func (s *GithubService) IsKnownInstallation(owner string) bool {
_, ok := s.cfg.Installations[owner]
return ok
}

func (s *GithubService) WithInstallationOwner(owner string) (*GithubService, error) {
installation, ok := s.cfg.Installations[owner]
if !ok {
return nil, fmt.Errorf("unknown installation owner: %s", owner)
}
client := github.NewClient(httpClient)
if token != nil {
client = client.WithAuthToken(*token)
hasToken = true

return s.WithInstallation(installation.ID)
}

func (s *GithubService) WithInstallation(installationID int64) (*GithubService, error) {
// load private key file from path
privateKey, err := os.ReadFile(s.cfg.PrivateKeyPath)
if err != nil {
return nil, err
}

var httpTransport http.RoundTripper
itr, err := ghinstallation.New(http.DefaultTransport, s.cfg.AppID, installationID, privateKey)
if err != nil {
return nil, err
}
httpTransport = itr

client := github.NewClient(internalhttp.NewHttpClient(&httpTransport))
return &GithubService{client: client}, nil
}

func (s *GithubService) WithAuthToken(ctx context.Context, token string) (*GithubService, error) {
client := s.client.WithAuthToken(token)
user, _, err := client.Users.Get(ctx, "")
if err != nil {
return nil, err
}

return &GithubService{
client: client,
hasToken: hasToken,
client: client,
user: user,
}, nil
}
7 changes: 1 addition & 6 deletions github/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,7 @@ func (h *GithubService) GetRepositories(ctx context.Context, owner string) ([]ho
return mapRepositories(owner, githubRepos), nil
}

var user *github.User
if h.hasToken {
user, _, _ = h.client.Users.Get(ctx, "")
}

if user != nil && user.GetLogin() == owner {
if h.user != nil && h.user.GetLogin() == owner {
githubRepos, _, err := h.client.Repositories.ListByAuthenticatedUser(ctx, &github.RepositoryListByAuthenticatedUserOptions{
Sort: "updated",
})
Expand Down
8 changes: 2 additions & 6 deletions gitlab/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ package gitlab

import (
"fmt"
"net/http"

"github.com/thegalactiks/giteway/hosting"
"github.com/thegalactiks/giteway/internal/http"
"github.com/xanzy/go-gitlab"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

type GitlabService struct {
Expand All @@ -16,10 +15,7 @@ type GitlabService struct {
var _ hosting.GitHostingService = (*GitlabService)(nil)

func NewGitlabService(token string) (*GitlabService, error) {
httpClient := &http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}
client, err := gitlab.NewOAuthClient(token, gitlab.WithHTTPClient(httpClient))
client, err := gitlab.NewOAuthClient(token, gitlab.WithHTTPClient(http.NewHttpClient(nil)))
if err != nil {
return nil, err
}
Expand Down
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/thegalactiks/giteway
go 1.22.0

require (
github.com/bradleyfalzon/ghinstallation/v2 v2.9.0
github.com/gin-contrib/cors v1.5.0
github.com/gin-contrib/requestid v0.0.6
github.com/gin-contrib/timeout v0.0.7
Expand Down Expand Up @@ -47,7 +48,9 @@ require (
github.com/go-playground/validator/v10 v10.18.0 // indirect
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/go-github/v57 v57.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/bradleyfalzon/ghinstallation/v2 v2.9.0 h1:HmxIYqnxubRYcYGRc5v3wUekmo5Wv2uX3gukmWJ0AFk=
github.com/bradleyfalzon/ghinstallation/v2 v2.9.0/go.mod h1:wmkTDJf8CmVypxE8ijIStFnKoTa6solK5QfdmJrP9KI=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE=
Expand Down Expand Up @@ -59,6 +61,8 @@ github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDs
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
Expand All @@ -67,6 +71,8 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v57 v57.0.0 h1:L+Y3UPTY8ALM8x+TV0lg+IEBI+upibemtBD8Q9u7zHs=
github.com/google/go-github/v57 v57.0.0/go.mod h1:s0omdnye0hvK/ecLvpsGfJMiRt85PimQh4oygmLIxHw=
github.com/google/go-github/v60 v60.0.0 h1:oLG98PsLauFvvu4D/YPxq374jhSxFYdzQGNCyONLfn8=
github.com/google/go-github/v60 v60.0.0/go.mod h1:ByhX2dP9XT9o/ll2yXAu2VD8l5eNVg8hD4Cr0S/LmQk=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
Expand Down
Loading

0 comments on commit 57ef91e

Please sign in to comment.