Skip to content

Commit

Permalink
feat(ghcr): Add support for GitHub app authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
gabe565 committed Feb 9, 2024
1 parent e4b98b1 commit 48df25e
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 15 deletions.
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ module github.com/gabe565/transsmute
go 1.21.6

require (
github.com/bradleyfalzon/ghinstallation/v2 v2.9.0
github.com/go-chi/chi/v5 v5.0.11
github.com/google/go-github/v58 v58.0.0
github.com/gorilla/feeds v1.1.2
github.com/heroku/docker-registry-client v0.0.0-20211012143308-9463674c8930
github.com/n0madic/twitter-scraper v0.0.0-20231104223941-296710769dd8
Expand All @@ -23,8 +25,11 @@ require (
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-logr/logr v1.3.0 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // 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/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
Expand Down
11 changes: 11 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2Aawl
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OpenPeeDeeP/depguard v1.0.0/go.mod h1:7/4sitnI9YlQgTLLk734QlzXT8DuHVnAyztLplQjk+o=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
github.com/bradleyfalzon/ghinstallation/v2 v2.9.0 h1:HmxIYqnxubRYcYGRc5v3wUekmo5Wv2uX3gukmWJ0AFk=
github.com/bradleyfalzon/ghinstallation/v2 v2.9.0/go.mod h1:wmkTDJf8CmVypxE8ijIStFnKoTa6solK5QfdmJrP9KI=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
Expand Down Expand Up @@ -57,6 +59,8 @@ github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslW
github.com/go-toolsmith/typep v1.0.0/go.mod h1:JSQCQMUPdRlMZFswiq3TGpNp1GMktqkR2Ns5AIQkATU=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
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/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
Expand Down Expand Up @@ -98,11 +102,18 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/v58 v58.0.0 h1:Una7GGERlF/37XfkPwpzYJe0Vp4dt2k1kCjlxwjIvzw=
github.com/google/go-github/v58 v58.0.0/go.mod h1:k4hxDKEfoWpSqFlc8LTpGd9fu2KrV1YAa6Hi6FmDNY4=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
Expand Down
7 changes: 6 additions & 1 deletion internal/docker/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,14 @@ func Handler(w http.ResponseWriter, r *http.Request) {
repo = reg.NormalizeRepo(repo)
owner := &feeds.Author{Name: reg.GetOwner(repo)}

tr, err := reg.Transport(r.Context(), repo)
if err != nil {
panic(err)
}

hub := registry.Registry{
URL: reg.ApiUrl(),
Client: &http.Client{Transport: reg.Transport(repo)},
Client: &http.Client{Transport: tr},
Logf: logrus.StandardLogger().Debugf,
}

Expand Down
21 changes: 16 additions & 5 deletions internal/docker/registry.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package docker

import (
"context"
"net/http"
"strings"
)
Expand All @@ -11,21 +12,31 @@ type Registry interface {
ApiUrl() string
TokenUrl(repo string) string

Transport(repo string) http.RoundTripper
Transport(ctx context.Context, repo string) (http.RoundTripper, error)

NormalizeRepo(repo string) string
GetRepoUrl(repo string) string
GetTagUrl(repo, tag string) string
GetOwner(repo string) string
}

var Registries = []Registry{
Ghcr{},
DockerHub{},
var registries []Registry

func SetupRegistries() error {
ghcr, err := NewGhcr()
if err != nil {
return err
}

registries = []Registry{
ghcr,
&DockerHub{},
}
return nil
}

func FindRegistry(repo string) Registry {
for _, registry := range Registries {
for _, registry := range registries {
if strings.HasPrefix(repo, registry.Name()) {
return registry
}
Expand Down
5 changes: 3 additions & 2 deletions internal/docker/registry_dockerhub.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package docker

import (
"context"
"net/http"
"strings"

Expand Down Expand Up @@ -35,13 +36,13 @@ func (d DockerHub) TokenUrl(repo string) string {
return "https://auth.docker.io/token?service=registry.hub.docker.com&scope=repository:" + repo + ":pull"
}

func (d DockerHub) Transport(repo string) http.RoundTripper {
func (d DockerHub) Transport(_ context.Context, repo string) (http.RoundTripper, error) {
return registry.WrapTransport(
http.DefaultTransport,
d.TokenUrl(repo),
viper.GetString("dockerhub.username"),
viper.GetString("dockerhub-password"),
)
), nil
}

func (d DockerHub) NormalizeRepo(repo string) string {
Expand Down
117 changes: 110 additions & 7 deletions internal/docker/registry_ghcr.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
package docker

import (
"context"
"net/http"
"os"
"strconv"
"strings"
"time"

"github.com/bradleyfalzon/ghinstallation/v2"
"github.com/google/go-github/v58/github"
"github.com/heroku/docker-registry-client/registry"
flag "github.com/spf13/pflag"
"github.com/spf13/viper"
)

type AuthMethod uint8

const (
AuthNone AuthMethod = iota
AuthToken
AuthApp
)

func init() {
flag.String("ghcr-username", "", "GitHub username for ghcr.io")
if err := viper.BindPFlag("ghcr.username", flag.Lookup("ghcr-username")); err != nil {
Expand All @@ -19,9 +33,79 @@ func init() {
if err := viper.BindPFlag("ghcr.password", flag.Lookup("ghcr-password")); err != nil {
panic(err)
}

flag.Int64("ghcr-app-id", 0, "GitHub app ID")
if err := viper.BindPFlag("ghcr.app-id", flag.Lookup("ghcr-app-id")); err != nil {
panic(err)
}

flag.Int64("ghcr-installation-id", 0, "GitHub installation ID")
if err := viper.BindPFlag("ghcr.installation-id", flag.Lookup("ghcr-installation-id")); err != nil {
panic(err)
}

flag.String("ghcr-private-key", "", "GitHub app private key")
if err := viper.BindPFlag("ghcr.private-key", flag.Lookup("ghcr-private-key")); err != nil {
panic(err)
}

flag.String("ghcr-private-key-path", "", "GitHub app private key file path")
if err := viper.BindPFlag("ghcr.private-key-path", flag.Lookup("ghcr-private-key-path")); err != nil {
panic(err)
}
}

type Ghcr struct{}
func NewGhcr() (*Ghcr, error) {
ghcr := &Ghcr{
username: viper.GetString("ghcr.username"),
password: viper.GetString("ghcr.password"),

installationId: viper.GetInt64("ghcr.installation-id"),
}
appId := viper.GetInt64("ghcr.app-id")
privateKey := []byte(viper.GetString("ghcr.private-key"))
if len(privateKey) == 0 {
privateKeyPath := viper.GetString("ghcr.private-key-path")
if privateKeyPath != "" {
var err error
privateKey, err = os.ReadFile(privateKeyPath)
if err != nil {
return ghcr, err
}
}
}

if ghcr.authMethod == AuthNone && appId != 0 && ghcr.installationId != 0 && len(privateKey) != 0 {
itr, err := ghinstallation.NewAppsTransport(http.DefaultTransport, appId, privateKey)
if err != nil {
return ghcr, err
}

ghcr.client = github.NewClient(&http.Client{Transport: itr})
ghcr.username = strconv.Itoa(int(ghcr.installationId))
ghcr.authMethod = AuthApp
if err := ghcr.RefreshAppToken(context.Background()); err != nil {
return ghcr, err
}
}

if ghcr.authMethod == AuthNone && ghcr.username != "" && ghcr.password != "" {
ghcr.authMethod = AuthToken
}

return ghcr, nil
}

type Ghcr struct {
authMethod AuthMethod

username string
password string

client *github.Client
installationId int64
refreshedAt time.Time
}

func (g Ghcr) Name() string {
return "ghcr.io"
Expand All @@ -35,16 +119,19 @@ func (g Ghcr) TokenUrl(repo string) string {
return g.ApiUrl() + "/token?service=ghcr.io&scope=repository:" + repo + ":pull"
}

func (g Ghcr) Transport(repo string) http.RoundTripper {
username := viper.GetString("ghcr.username")
password := viper.GetString("ghcr.password")
func (g Ghcr) Transport(ctx context.Context, repo string) (http.RoundTripper, error) {
if g.authMethod == AuthApp && time.Since(g.refreshedAt) >= 45*time.Minute {
if err := g.RefreshAppToken(ctx); err != nil {
return nil, err
}
}

return registry.WrapTransport(
http.DefaultTransport,
g.TokenUrl(repo),
username,
password,
)
g.username,
g.password,
), nil
}

func (g Ghcr) NormalizeRepo(repo string) string {
Expand All @@ -62,3 +149,19 @@ func (g Ghcr) GetTagUrl(repo, tag string) string {
func (g Ghcr) GetOwner(repo string) string {
return strings.SplitN(repo, "/", 3)[1]
}

func (g *Ghcr) RefreshAppToken(ctx context.Context) error {
g.refreshedAt = time.Now()

token, _, err := g.client.Apps.CreateInstallationToken(ctx, g.installationId, &github.InstallationTokenOptions{
Permissions: &github.InstallationPermissions{
Packages: github.String("read"),
},
})
if err != nil {
return err
}

g.password = token.GetToken()
return nil
}
5 changes: 5 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"net/http"
"strings"

"github.com/gabe565/transsmute/internal/docker"
"github.com/gabe565/transsmute/internal/server"
flag "github.com/spf13/pflag"
"github.com/spf13/viper"
Expand Down Expand Up @@ -40,6 +41,10 @@ func main() {

flag.Parse()

if err := docker.SetupRegistries(); err != nil {
panic(err)
}

s := server.New()
address := viper.GetString("address")
log.Println("Listening on " + address)
Expand Down

0 comments on commit 48df25e

Please sign in to comment.