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  }