github.com/caos/orbos@v1.5.14-0.20221103111702-e6cd0cea7ad4/internal/stores/github/github.go (about) 1 package github 2 3 import ( 4 "bufio" 5 "context" 6 "fmt" 7 "io/ioutil" 8 "os" 9 "path/filepath" 10 "strings" 11 "syscall" 12 13 "github.com/caos/oidc/pkg/client/rp" 14 "github.com/caos/oidc/pkg/client/rp/cli" 15 httphelper "github.com/caos/oidc/pkg/http" 16 "github.com/caos/oidc/pkg/oidc" 17 "github.com/caos/orbos/mntr" 18 "github.com/ghodss/yaml" 19 "github.com/google/go-github/v31/github" 20 "github.com/google/uuid" 21 "golang.org/x/crypto/ssh/terminal" 22 "golang.org/x/oauth2" 23 githubOAuth "golang.org/x/oauth2/github" 24 25 "github.com/caos/orbos/internal/utils/helper" 26 helperpkg "github.com/caos/orbos/pkg/helper" 27 ) 28 29 type githubAPI struct { 30 monitor mntr.Monitor 31 client *github.Client 32 status error 33 } 34 35 func (g *githubAPI) GetStatus() error { 36 return g.status 37 } 38 39 func New(monitor mntr.Monitor) *githubAPI { 40 githubMonitor := monitor.WithFields(map[string]interface{}{ 41 "store": "github", 42 }) 43 return &githubAPI{ 44 client: nil, 45 status: nil, 46 monitor: githubMonitor, 47 } 48 } 49 50 func (g *githubAPI) IsLoggedIn() bool { 51 return g.client != nil 52 } 53 54 func (g *githubAPI) Login() *githubAPI { 55 r := bufio.NewReader(os.Stdin) 56 fmt.Print("GitHub Username: ") 57 username, _ := r.ReadString('\n') 58 59 fmt.Print("GitHub Password: ") 60 bytePassword, _ := terminal.ReadPassword(int(syscall.Stdin)) 61 password := string(bytePassword) 62 63 g.LoginBasicAuth(username, password) 64 65 // Is this a two-factor auth error? If so, prompt for OTP and try again. 66 if _, ok := g.status.(*github.TwoFactorAuthError); ok { 67 g.status = nil 68 69 fmt.Print("\nGitHub OTP: ") 70 otp, _ := r.ReadString('\n') 71 72 g.LoginTwoFactor(username, password, otp) 73 if g.GetStatus() != nil { 74 return g 75 } 76 } else if g.status != nil { 77 g.client = nil 78 } 79 80 return g 81 } 82 83 const ( 84 githubToken = "ghtoken" 85 ) 86 87 func (g *githubAPI) LoginOAuth(ctx context.Context, folderPath string, clientID, clientSecret string) *githubAPI { 88 filePath := filepath.Join(folderPath, githubToken) 89 port := "9999" 90 callbackPath := "/orbctl/github/callback" 91 92 rpConfig := &oauth2.Config{ 93 ClientID: clientID, 94 ClientSecret: clientSecret, 95 RedirectURL: fmt.Sprintf("http://localhost:%v%v", port, callbackPath), 96 Scopes: []string{"repo", "repo_deployment"}, 97 Endpoint: githubOAuth.Endpoint, 98 } 99 100 key := helperpkg.RandStringBytes(32) 101 cookieHandler := httphelper.NewCookieHandler([]byte(key), []byte(key), httphelper.WithUnsecure()) 102 relyingParty, err := rp.NewRelyingPartyOAuth(rpConfig, rp.WithCookieHandler(cookieHandler)) 103 if err != nil { 104 panic(fmt.Errorf("error creating relaying party: %w", err)) 105 } 106 107 makeClient := func(token *oidc.Tokens) error { 108 g.client = github.NewClient(relyingParty.OAuthConfig().Client(ctx, token.Token)) 109 _, _, err = g.client.Users.Get(ctx, "") 110 if err != nil { 111 g.status = err 112 g.client = nil 113 } 114 return g.status 115 } 116 117 if err := clientFromCache(filePath, makeClient); err != nil { 118 119 g.monitor.WithField("reason", err.Error()).Info("Trying CodeFlow as reusing an existing token failed") 120 121 token := cli.CodeFlow(ctx, relyingParty, callbackPath, port, uuid.NewString) 122 123 makeClient(token) 124 if g.status != nil { 125 g.status = fmt.Errorf("CodeFlow failed: %w", g.status) 126 return g 127 } 128 g.monitor.Info("CodeFlow succeeded") 129 130 data, err := yaml.Marshal(token) 131 if err != nil { 132 g.status = err 133 return g 134 } 135 136 if err := ioutil.WriteFile(filePath, data, os.ModePerm); err != nil { 137 g.status = err 138 return g 139 } 140 } 141 return g 142 } 143 144 func clientFromCache(filePath string, makeClient func(token *oidc.Tokens) error) error { 145 if !helper.FileExists(filePath) { 146 return fmt.Errorf("file %s does not exist", filePath) 147 } 148 token := new(oidc.Tokens) 149 150 data, err := ioutil.ReadFile(filePath) 151 if err != nil { 152 return err 153 } 154 155 if err := yaml.Unmarshal(data, token); err != nil { 156 return err 157 } 158 159 if err := makeClient(token); err != nil { 160 if rmErr := os.Remove(filePath); rmErr != nil { 161 panic(rmErr) 162 } 163 } 164 return err 165 } 166 167 func (g *githubAPI) LoginToken(token string) *githubAPI { 168 if g.status != nil { 169 return g 170 } 171 172 ctx := context.Background() 173 ts := oauth2.StaticTokenSource( 174 &oauth2.Token{AccessToken: token}, 175 ) 176 tc := oauth2.NewClient(ctx, ts) 177 178 client := github.NewClient(tc) 179 _, _, g.status = client.Users.Get(ctx, "") 180 if g.GetStatus() != nil { 181 return g 182 } 183 184 g.monitor.Info("PersonalAccessTokenFlow succeeded") 185 g.client = client 186 return g 187 } 188 189 func (g *githubAPI) LoginBasicAuth(username, password string) *githubAPI { 190 if g.status != nil { 191 return g 192 } 193 194 tp := github.BasicAuthTransport{ 195 Username: strings.TrimSpace(username), 196 Password: strings.TrimSpace(password), 197 } 198 199 client := github.NewClient(tp.Client()) 200 201 ctx := context.Background() 202 _, _, g.status = client.Users.Get(ctx, "") 203 if g.GetStatus() != nil { 204 return g 205 } 206 207 g.monitor.Info("BasicAuthFlow succeeded") 208 g.client = client 209 return g 210 } 211 212 func (g *githubAPI) LoginTwoFactor(username, password, twoFactor string) *githubAPI { 213 if g.status != nil { 214 return g 215 } 216 217 tp := github.BasicAuthTransport{ 218 Username: strings.TrimSpace(username), 219 Password: strings.TrimSpace(password), 220 OTP: strings.TrimSpace(twoFactor), 221 } 222 223 client := github.NewClient(tp.Client()) 224 225 ctx := context.Background() 226 _, _, g.status = client.Users.Get(ctx, "") 227 if g.GetStatus() != nil { 228 return g 229 } 230 231 g.monitor.Info("BasicAuthFlow with OTP succeeded") 232 g.client = client 233 return g 234 } 235 236 func (g *githubAPI) GetRepositorySSH(url string) (*github.Repository, error) { 237 if g.GetStatus() != nil { 238 return nil, g.status 239 } 240 241 ctx := context.Background() 242 parts := strings.Split(strings.TrimPrefix(url, "git@github.com:"), "/") 243 244 repo, _, err := g.client.Repositories.Get(ctx, parts[0], strings.TrimSuffix(parts[1], ".git")) 245 if err != nil { 246 g.status = err 247 } 248 return repo, err 249 } 250 251 func (g *githubAPI) GetRepositories() ([]*github.Repository, error) { 252 if g.GetStatus() != nil { 253 return nil, g.status 254 } 255 256 ctx := context.Background() 257 repos := make([]*github.Repository, 0) 258 addRepos, err := addRepositories(ctx, g.client, "private", "owner") 259 if err != nil { 260 g.status = err 261 return nil, err 262 } 263 repos = append(repos, addRepos...) 264 265 addRepos, err = addRepositories(ctx, g.client, "public", "owner") 266 if err != nil { 267 g.status = err 268 return nil, err 269 } 270 repos = append(repos, addRepos...) 271 272 addRepos, err = addRepositories(ctx, g.client, "private", "organization_member") 273 if err != nil { 274 g.status = err 275 return nil, err 276 } 277 repos = append(repos, addRepos...) 278 279 addRepos, err = addRepositories(ctx, g.client, "public", "organization_member") 280 if err != nil { 281 g.status = err 282 return nil, err 283 } 284 repos = append(repos, addRepos...) 285 286 addRepos, err = addRepositories(ctx, g.client, "private", "collaborator") 287 if err != nil { 288 g.status = err 289 return nil, err 290 } 291 repos = append(repos, addRepos...) 292 293 addRepos, err = addRepositories(ctx, g.client, "public", "collaborator") 294 if err != nil { 295 g.status = err 296 return nil, err 297 } 298 repos = append(repos, addRepos...) 299 300 return repos, nil 301 } 302 303 func addRepositories(ctx context.Context, client *github.Client, visibility, affiliation string) ([]*github.Repository, error) { 304 opts := &github.RepositoryListOptions{ 305 Visibility: visibility, 306 Affiliation: affiliation, 307 } 308 309 addRepos, _, err := client.Repositories.List(ctx, "", opts) 310 return addRepos, err 311 } 312 313 func (g *githubAPI) getDeployKeys(repo *github.Repository) []*github.Key { 314 if g.GetStatus() != nil { 315 return nil 316 } 317 318 ctx := context.Background() 319 320 keys, _, err := g.client.Repositories.ListKeys(ctx, *repo.Owner.Login, *repo.Name, nil) 321 if err != nil { 322 g.status = err 323 return nil 324 } 325 return keys 326 } 327 328 func (g *githubAPI) CreateDeployKey(repo *github.Repository, value string) *githubAPI { 329 if g.GetStatus() != nil { 330 return g 331 } 332 ctx := context.Background() 333 334 f := false 335 key := github.Key{ 336 Key: &value, 337 Title: strPtr("orbos-system"), 338 ReadOnly: &f, 339 } 340 341 _, _, g.status = g.client.Repositories.CreateKey(ctx, *repo.Owner.Login, *repo.Name, &key) 342 343 return g 344 } 345 346 func (g *githubAPI) EnsureNoDeployKey(repo *github.Repository) *githubAPI { 347 if g.GetStatus() != nil { 348 return g 349 } 350 ctx := context.Background() 351 keys := g.getDeployKeys(repo) 352 if g.status != nil { 353 return g 354 } 355 356 for _, key := range keys { 357 if *key.Title == "orbos-system" { 358 if _, g.status = g.client.Repositories.DeleteKey(ctx, *repo.Owner.Login, *repo.Name, *key.ID); g.status != nil { 359 return g 360 } 361 } 362 } 363 364 return g 365 } 366 367 func strPtr(str string) *string { 368 return &str 369 }