-
Notifications
You must be signed in to change notification settings - Fork 6
/
main.go
198 lines (156 loc) · 5.53 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
package main
import (
"encoding/json"
"fmt"
jwt "github.com/dgrijalva/jwt-go"
flag "github.com/spf13/pflag"
"io/ioutil"
logger "log"
"net/http"
"os"
"strings"
"time"
)
type installationToken struct {
Token string `json:"token"`
ExpiresAt string `json:"expires_at"`
}
type installation struct {
ID int `json:"id"`
AccessTokensURL string `json:"access_tokens_url"`
RepositoriesURL string `json:"repositories_url"`
}
type repositories struct {
List []struct {
Name string `json:"name"`
Owner struct {
Login string `json:"login"`
} `json:"owner"`
} `json:"repositories"`
}
type config struct {
apiURL string
appID string
keyPath string
installID string
repoOwner string
repoName string
}
var verbose bool
func main() {
var cfg = parseFlags()
jwtToken := getJwtToken(cfg.appID, cfg.keyPath)
var token string
if cfg.installID == "" && cfg.repoOwner == "" {
log("Generated JWT token for app ID %s\n", cfg.appID)
token = jwtToken
} else if cfg.installID != "" {
installToken := getInstallationToken(cfg.apiURL, jwtToken, cfg.appID, cfg.installID)
log("Generated installation token for app ID %s and installation ID %s that expires at %s\n", cfg.appID, cfg.installID, installToken.ExpiresAt)
token = installToken.Token
} else {
installToken, err := getInstallationTokenForRepo(cfg.apiURL, jwtToken, cfg.appID, cfg.repoOwner, cfg.repoName)
handleErrorIfAny(err)
log("Generated installation token for app ID %s and repo %s/%s that expires at %s\n", cfg.appID, cfg.repoOwner, cfg.repoName, installToken.ExpiresAt)
token = installToken.Token
}
fmt.Print(token)
}
func parseFlags() config {
var cfg config
flag.StringVarP(&cfg.apiURL, "apiUrl", "g", "https://api.github.com", "GitHub API URL")
flag.StringVarP(&cfg.appID, "appId", "a", "", "Appliction ID as defined in app settings (Required)")
flag.StringVarP(&cfg.keyPath, "keyPath", "k", "", "Path to key PEM file generated in app settings (Required)")
flag.StringVarP(&cfg.installID, "installId", "i", "", "Installation ID of the application")
repoPtr := flag.StringP("repo", "r", "", "{owner/repo} of the GitHub repository")
flag.BoolVarP(&verbose, "verbose", "v", false, "Verbose stderr")
flag.Parse()
if len(os.Args) == 1 {
usage("")
}
if cfg.appID == "" {
usage("appId is required")
}
if cfg.keyPath == "" {
usage("keyPath is required")
}
if *repoPtr != "" {
repoInfo := strings.Split(*repoPtr, "/")
if len(repoInfo) != 2 {
usage("repo argument value must be owner/repo but was: " + *repoPtr)
}
cfg.repoOwner, cfg.repoName = repoInfo[0], repoInfo[1]
}
return cfg
}
func getJwtToken(appID string, keyPath string) string {
keyBytes, err := ioutil.ReadFile(keyPath)
handleErrorIfAny(err)
signKey, err := jwt.ParseRSAPrivateKeyFromPEM(keyBytes)
handleErrorIfAny(err)
now := time.Now()
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
"iat": now.Unix(),
"exp": now.Add(time.Minute * 10).Unix(),
"iss": appID,
})
jwtTokenString, err := token.SignedString(signKey)
handleErrorIfAny(err)
return jwtTokenString
}
func httpJSON(method string, url string, authorization string, result interface{}) {
client := &http.Client{}
req, err := http.NewRequest(method, url, nil)
handleErrorIfAny(err)
req.Header.Add("Authorization", authorization)
req.Header.Add("Accept", "application/vnd.github.machine-man-preview+json")
log("GitHub request: %s", req)
resp, err := client.Do(req)
handleErrorIfAny(err)
defer resp.Body.Close()
respData, err := ioutil.ReadAll(resp.Body)
handleErrorIfAny(err)
log("GitHub response: %s", respData)
json.Unmarshal(respData, &result)
log("%s", result)
}
func getInstallationToken(apiURL string, jwtToken string, appID string, installationID string) installationToken {
var token installationToken
httpJSON("POST", fmt.Sprintf("%s/app/installations/%s/access_tokens", apiURL, installationID), "Bearer "+jwtToken, &token)
return token
}
func getInstallationTokenForRepo(apiURL string, jwtToken string, appID string, owner string, repo string) (installationToken, error) {
var installations []installation
httpJSON("GET", apiURL+"/app/installations", "Bearer "+jwtToken, &installations)
for _, installation := range installations {
var token installationToken
httpJSON("POST", installation.AccessTokensURL, "Bearer "+jwtToken, &token)
var repos repositories
httpJSON("GET", installation.RepositoriesURL, "token "+token.Token, &repos)
for _, repository := range repos.List {
if owner == repository.Owner.Login && repo == repository.Name {
return token, nil
}
}
}
var empty installationToken
return empty, fmt.Errorf("Unable to find repository %s/%s in installations of app ID %s", owner, repo, appID)
}
func log(format string, v ...interface{}) {
if verbose {
logger.Printf(format, v...)
}
}
func handleErrorIfAny(err error) {
if err != nil {
logger.Fatalln(err)
}
}
func usage(msg string) {
if msg != "" {
fmt.Fprintf(os.Stderr, "ERROR: %s\n\n", msg)
}
fmt.Fprintf(os.Stderr, "Usage: gha-token [flags]\n\nFlags:\n")
flag.PrintDefaults()
os.Exit(1)
}