瀏覽代碼

temp commit to remove docker/.env

Alexander Belanger 5 年之前
父節點
當前提交
568d83cd0f

+ 15 - 1
cmd/app/main.go

@@ -6,6 +6,7 @@ import (
 	"net/http"
 
 	"github.com/gorilla/sessions"
+	"github.com/porter-dev/porter/internal/oauth"
 	"github.com/porter-dev/porter/internal/repository/gorm"
 
 	"github.com/porter-dev/porter/server/api"
@@ -52,7 +53,20 @@ func main() {
 
 	validator := vr.New()
 
-	a := api.New(logger, repo, validator, store, appConf.Server.CookieName, false)
+	a := api.New(
+		logger,
+		repo,
+		validator,
+		store,
+		appConf.Server.CookieName,
+		false,
+		&oauth.Config{
+			ClientID:     appConf.Server.GithubClientID,
+			ClientSecret: appConf.Server.GithubClientSecret,
+			Scopes:       []string{"repo", "user", "read:user"},
+			BaseURL:      appConf.Server.ServerURL,
+		},
+	)
 
 	appRouter := router.New(a, store, appConf.Server.CookieName, appConf.Server.StaticFilePath, repo)
 

+ 0 - 17
docker/.env

@@ -1,17 +0,0 @@
-DEBUG=true
-
-STATIC_FILE_PATH=/porter/static
-
-SERVER_PORT=8080
-SERVER_TIMEOUT_READ=5s
-SERVER_TIMEOUT_WRITE=10s
-SERVER_TIMEOUT_IDLE=15s
-
-DB_HOST=postgres
-DB_PORT=5432
-DB_USER=porter
-DB_PASS=porter
-DB_NAME=porter
-COOKIE_SECRETS=secret
-
-SQL_LITE=false

+ 2 - 0
go.mod

@@ -24,6 +24,8 @@ require (
 	github.com/go-playground/validator/v10 v10.3.0
 	github.com/go-test/deep v1.0.7
 	github.com/google/go-cmp v0.5.1
+	github.com/google/go-github v17.0.0+incompatible
+	github.com/google/go-querystring v1.0.0 // indirect
 	github.com/gorilla/securecookie v1.1.1
 	github.com/gorilla/sessions v1.2.1
 	github.com/imdario/mergo v0.3.11 // indirect

+ 4 - 0
go.sum

@@ -401,6 +401,10 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k=
 github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
+github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ=
+github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
+github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g=
 github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=

+ 4 - 0
internal/config/config.go

@@ -17,6 +17,7 @@ type Conf struct {
 
 // ServerConf is the server configuration
 type ServerConf struct {
+	ServerURL      string        `env:"SERVER_URL,default=http://localhost:8080"`
 	Port           int           `env:"SERVER_PORT,default=8080"`
 	StaticFilePath string        `env:"STATIC_FILE_PATH,default=/porter/static"`
 	CookieName     string        `env:"COOKIE_NAME,default=porter"`
@@ -24,6 +25,9 @@ type ServerConf struct {
 	TimeoutRead    time.Duration `env:"SERVER_TIMEOUT_READ,default=5s"`
 	TimeoutWrite   time.Duration `env:"SERVER_TIMEOUT_WRITE,default=10s"`
 	TimeoutIdle    time.Duration `env:"SERVER_TIMEOUT_IDLE,default=15s"`
+
+	GithubClientID     string `env:"GITHUB_CLIENT_ID"`
+	GithubClientSecret string `env:"GITHUB_CLIENT_SECRET"`
 }
 
 // DBConf is the database configuration: if generated from environment variables,

+ 18 - 8
internal/models/project.go

@@ -8,17 +8,20 @@ import (
 type Project struct {
 	gorm.Model
 
-	Name                     string                    `json:"name"`
-	Roles                    []Role                    `json:"roles"`
+	Name        string       `json:"name"`
+	Roles       []Role       `json:"roles"`
+	RepoClients []RepoClient `json:"repo_clients"`
+
 	ServiceAccountCandidates []ServiceAccountCandidate `json:"sa_candidates"`
 	ServiceAccounts          []ServiceAccount          `json:"serviceaccounts"`
 }
 
 // ProjectExternal represents the Project type that is sent over REST
 type ProjectExternal struct {
-	ID    uint           `json:"id"`
-	Name  string         `json:"name"`
-	Roles []RoleExternal `json:"roles"`
+	ID          uint                 `json:"id"`
+	Name        string               `json:"name"`
+	Roles       []RoleExternal       `json:"roles"`
+	RepoClients []RepoClientExternal `json:"repo_clients"`
 }
 
 // Externalize generates an external Project to be shared over REST
@@ -29,9 +32,16 @@ func (p *Project) Externalize() *ProjectExternal {
 		roles = append(roles, *role.Externalize())
 	}
 
+	repoClients := make([]RepoClientExternal, 0)
+
+	for _, repoClient := range p.RepoClients {
+		repoClients = append(repoClients, *repoClient.Externalize())
+	}
+
 	return &ProjectExternal{
-		ID:    p.ID,
-		Name:  p.Name,
-		Roles: roles,
+		ID:          p.ID,
+		Name:        p.Name,
+		Roles:       roles,
+		RepoClients: repoClients,
 	}
 }

+ 50 - 0
internal/models/repoclient.go

@@ -0,0 +1,50 @@
+package models
+
+import (
+	"strings"
+
+	"gorm.io/gorm"
+)
+
+// The allowed repository clients
+const (
+	RepoClientGithub = "github"
+)
+
+// RepoClient is a client for a set of repositories that has been added
+// via a project OAuth flow
+type RepoClient struct {
+	gorm.Model
+
+	ProjectID uint `json:"project_id"`
+
+	// the kind can be one of the predefined repo kinds
+	Kind         string `json:"kind"`
+	Repositories string `json:"repositories"`
+
+	// ------------------------------------------------------------------
+	// All fields below this line are encrypted before storage
+	// ------------------------------------------------------------------
+
+	AccessToken  string `json:"access_token"`
+	RefreshToken string `json:"refresh_token"`
+}
+
+// RepoClientExternal is a RepoClient scrubbed of sensitive information to be
+// shared over REST
+type RepoClientExternal struct {
+	ID           uint     `json:"id"`
+	ProjectID    uint     `json:"project_id"`
+	Kind         string   `json:"kind"`
+	Repositories []string `json:"repositories"`
+}
+
+// Externalize generates an external RepoClient to be shared over REST
+func (r *RepoClient) Externalize() *RepoClientExternal {
+	return &RepoClientExternal{
+		ID:           r.Model.ID,
+		ProjectID:    r.ProjectID,
+		Kind:         r.Kind,
+		Repositories: strings.Split(r.Repositories, ","),
+	}
+}

+ 37 - 0
internal/oauth/config.go

@@ -0,0 +1,37 @@
+package oauth
+
+import (
+	"crypto/rand"
+	"encoding/base64"
+
+	"golang.org/x/oauth2"
+)
+
+type Config struct {
+	ClientID     string
+	ClientSecret string
+	Scopes       []string
+	BaseURL      string
+}
+
+func NewGithubClient(cfg *Config) *oauth2.Config {
+	return &oauth2.Config{
+		ClientID:     cfg.ClientID,
+		ClientSecret: cfg.ClientSecret,
+		Endpoint: oauth2.Endpoint{
+			AuthURL:  "https://github.com/login/oauth/authorize",
+			TokenURL: "https://github.com/login/oauth/access_token",
+		},
+		RedirectURL: cfg.BaseURL + "/api/auth/callback/github",
+		Scopes:      cfg.Scopes,
+	}
+}
+
+func CreateRandomState() string {
+	b := make([]byte, 16)
+	rand.Read(b)
+
+	state := base64.URLEncoding.EncodeToString(b)
+
+	return state
+}

+ 11 - 0
internal/repository/repoclient.go

@@ -0,0 +1,11 @@
+package repository
+
+import "github.com/porter-dev/porter/internal/models"
+
+// RepoClientRepository represents the set of queries on the
+// RepoClient model
+type RepoClientRepository interface {
+	CreateRepoClient(rc *models.RepoClient) (*models.RepoClient, error)
+	ReadRepoClient(id uint) (*models.RepoClient, error)
+	ListRepoClientsByProjectID(projectID uint) ([]*models.RepoClient, error)
+}

+ 1 - 0
internal/repository/repository.go

@@ -6,4 +6,5 @@ type Repository struct {
 	Project        ProjectRepository
 	Session        SessionRepository
 	ServiceAccount ServiceAccountRepository
+	RepoClient     RepoClientRepository
 }

+ 66 - 0
internal/repository/test/repoclient.go

@@ -0,0 +1,66 @@
+package test
+
+import (
+	"errors"
+
+	"github.com/porter-dev/porter/internal/models"
+	"github.com/porter-dev/porter/internal/repository"
+	"gorm.io/gorm"
+)
+
+// RepoClientRepository implements repository.RepoClientRepository
+type RepoClientRepository struct {
+	canQuery    bool
+	repoClients []*models.RepoClient
+}
+
+// NewRepoClientRepository will return errors if canQuery is false
+func NewRepoClientRepository(canQuery bool) repository.RepoClientRepository {
+	return &RepoClientRepository{
+		canQuery,
+		[]*models.RepoClient{},
+	}
+}
+
+// CreateRepoClient creates a new repo client and appends it to the in-memory list
+func (repo *RepoClientRepository) CreateRepoClient(rc *models.RepoClient) (*models.RepoClient, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot write database")
+	}
+
+	repo.repoClients = append(repo.repoClients, rc)
+	rc.ID = uint(len(repo.repoClients))
+
+	return rc, nil
+}
+
+// ReadRepoClient returns a repo client by id
+func (repo *RepoClientRepository) ReadRepoClient(id uint) (*models.RepoClient, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	if int(id-1) >= len(repo.repoClients) || repo.repoClients[id-1] == nil {
+		return nil, gorm.ErrRecordNotFound
+	}
+
+	index := int(id - 1)
+	return repo.repoClients[index], nil
+}
+
+// ListRepoClientsByProjectID returns a list of repo clients that match a project id
+func (repo *RepoClientRepository) ListRepoClientsByProjectID(projectID uint) ([]*models.RepoClient, error) {
+	if !repo.canQuery {
+		return nil, errors.New("Cannot read from database")
+	}
+
+	res := make([]*models.RepoClient, 0)
+
+	for _, rc := range repo.repoClients {
+		if rc.ProjectID == projectID {
+			res = append(res, rc)
+		}
+	}
+
+	return res, nil
+}

+ 21 - 16
server/api/api.go

@@ -4,6 +4,8 @@ import (
 	"github.com/go-playground/locales/en"
 	ut "github.com/go-playground/universal-translator"
 	"github.com/go-playground/validator/v10"
+	"github.com/porter-dev/porter/internal/oauth"
+	"golang.org/x/oauth2"
 
 	"github.com/gorilla/sessions"
 	"github.com/porter-dev/porter/internal/helm"
@@ -23,14 +25,15 @@ type TestAgents struct {
 // App represents an API instance with handler methods attached, a DB connection
 // and a logger instance
 type App struct {
-	logger     *lr.Logger
-	repo       *repository.Repository
-	validator  *validator.Validate
-	store      sessions.Store
-	translator *ut.Translator
-	cookieName string
-	testing    bool
-	TestAgents *TestAgents
+	logger       *lr.Logger
+	repo         *repository.Repository
+	validator    *validator.Validate
+	store        sessions.Store
+	translator   *ut.Translator
+	cookieName   string
+	testing      bool
+	TestAgents   *TestAgents
+	GithubConfig *oauth2.Config
 }
 
 // New returns a new App instance
@@ -41,6 +44,7 @@ func New(
 	store sessions.Store,
 	cookieName string,
 	testing bool,
+	githubConfig *oauth.Config,
 ) *App {
 	// for now, will just support the english translator from the
 	// validator/translations package
@@ -61,14 +65,15 @@ func New(
 	}
 
 	return &App{
-		logger:     logger,
-		repo:       repo,
-		validator:  validator,
-		store:      store,
-		translator: &trans,
-		cookieName: cookieName,
-		testing:    testing,
-		TestAgents: testAgents,
+		logger:       logger,
+		repo:         repo,
+		validator:    validator,
+		store:        store,
+		translator:   &trans,
+		cookieName:   cookieName,
+		testing:      testing,
+		TestAgents:   testAgents,
+		GithubConfig: oauth.NewGithubClient(githubConfig),
 	}
 }
 

+ 1 - 1
server/api/helpers_test.go

@@ -79,7 +79,7 @@ func newTester(canQuery bool) *tester {
 	repo := test.NewRepository(canQuery)
 
 	store, _ := sessionstore.NewStore(repo, appConf.Server)
-	app := api.New(logger, repo, validator, store, appConf.Server.CookieName, true)
+	app := api.New(logger, repo, validator, store, appConf.Server.CookieName, true, nil)
 	r := router.New(app, store, appConf.Server.CookieName, appConf.Server.StaticFilePath, repo)
 
 	return &tester{

+ 170 - 0
server/api/oauth_github_handler.go

@@ -0,0 +1,170 @@
+package api
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"strconv"
+
+	"github.com/go-chi/chi"
+	"github.com/google/go-github/github"
+	"github.com/porter-dev/porter/internal/oauth"
+	"golang.org/x/oauth2"
+)
+
+// There is a difference between the oauth flow when logging a user in, and when
+// linking a repository.
+
+// When logging a user in, the access token gets stored in the session, and no refresh
+// token is requested. We store the access token in the session because a user can be
+// logged in multiple times with a single access token.
+
+// NOTE: this user flow will likely be augmented with Dex, or entirely replaced with Dex.
+
+// However, when linking a repository, the access token and refresh token are requested when
+// the flow has started. A project also gets linked to the session. After callback, a new
+// github config gets stored for the project, and the user will then get redirected to
+// a URL that allows them to select their repositories they'd like to link. We require a refresh
+// token because we need permanent access to the linked repository.
+
+func (app *App) HandleOAuthStartUser(w http.ResponseWriter, r *http.Request) {
+	state := oauth.CreateRandomState()
+
+	err := app.populateOAuthSession(w, r, state, false)
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	// specify access type offline to get a refresh token
+	url := app.GithubConfig.AuthCodeURL(state, oauth2.AccessTypeOnline)
+
+	http.Redirect(w, r, url, 302)
+}
+
+func (app *App) HandleOAuthStartProject(w http.ResponseWriter, r *http.Request) {
+	state := oauth.CreateRandomState()
+
+	err := app.populateOAuthSession(w, r, state, true)
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	// specify access type offline to get a refresh token
+	url := app.GithubConfig.AuthCodeURL(state, oauth2.AccessTypeOffline)
+
+	http.Redirect(w, r, url, 302)
+}
+
+func (app *App) HandleOauthCallback(w http.ResponseWriter, r *http.Request) {
+	session, err := app.store.Get(r, app.cookieName)
+
+	if err != nil {
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	if _, ok := session.Values["state"]; !ok {
+		// TODO -- SEND A CUSTOM ERROR, PROBABLY MEANS COOKIES ARE NOT ENABLED
+		// FOR NOW JUST SEND DATA READ ERROR
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	if r.URL.Query().Get("state") != session.Values["state"] {
+		// TODO -- SEND A CUSTOM ERROR, THIS MEANS THAT IDP CANNOT BE TRUSTED
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	token, err := app.GithubConfig.Exchange(oauth2.NoContext, r.URL.Query().Get("code"))
+
+	if err != nil {
+		// TODO -- SEND A CUSTOM ERROR
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	if !token.Valid() {
+		// TODO -- SEND A CUSTOM ERROR
+		app.handleErrorDataRead(err, w)
+		return
+	}
+
+	app.updateProjectFromToken(token)
+
+	// client := github.NewClient(app.GithubConfig.Client(oauth2.NoContext, token))
+
+	// client.
+
+	// TODO -- determine if the user already exists as a github user with that email
+	// If the user does not exist, create the user in the database
+
+	// If the user does exist, save the username, kind, and access_token in the session
+
+	// user, _, err := client.Users.Get(context.Background(), "")
+	// if err != nil {
+	// 	fmt.Println(w, "error getting name")
+	// 	return
+	// }
+
+	// session.Values["githubUserName"] = user.Name
+	// session.Values["githubAccessToken"] = token
+	// session.Save(r, w)
+
+	http.Redirect(w, r, "/", 302)
+}
+
+func (app *App) populateOAuthSession(w http.ResponseWriter, r *http.Request, state string, isProject bool) error {
+
+	session, err := app.store.Get(r, app.cookieName)
+
+	if err != nil {
+		return err
+	}
+
+	// need state parameter to validate when redirected
+	session.Values["state"] = state
+
+	if isProject {
+		// read the project id and add it to the session
+		projID, err := strconv.ParseUint(chi.URLParam(r, "project_id"), 0, 64)
+
+		if err != nil || projID == 0 {
+			return fmt.Errorf("could not read project id")
+		}
+
+		session.Values["project_id"] = projID
+	}
+
+	if err := session.Save(r, w); err != nil {
+		app.logger.Warn().Err(err)
+	}
+
+	return nil
+}
+
+func (app *App) upsertUserFromToken() error {
+	return fmt.Errorf("UNIMPLEMENTED")
+}
+
+// updates a project's repository clients with the token information.
+func (app *App) updateProjectFromToken(tok *oauth2.Token) error {
+	// get the list of repositories that this token has access to
+	client := github.NewClient(app.GithubConfig.Client(oauth2.NoContext, tok))
+
+	repos, _, err := client.Repositories.List(context.Background(), "", nil)
+
+	if err != nil {
+		return err
+	}
+
+	for _, repo := range repos {
+		fmt.Println(repo.GetName())
+	}
+
+	return fmt.Errorf("UNIMPLEMENTED")
+}