Skip to content

Commit

Permalink
feat: cli auth repo install flow
Browse files Browse the repository at this point in the history
  • Loading branch information
plyr4 committed Oct 8, 2024
1 parent 4834c0d commit 37b30a1
Show file tree
Hide file tree
Showing 11 changed files with 373 additions and 9 deletions.
29 changes: 27 additions & 2 deletions api/auth/get_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import (
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"

"github.com/go-vela/server/api"
"github.com/go-vela/server/api/types"
"github.com/go-vela/server/database"
"github.com/go-vela/server/internal"
"github.com/go-vela/server/internal/token"
"github.com/go-vela/server/scm"
"github.com/go-vela/server/util"
Expand All @@ -37,6 +39,10 @@ import (
// name: redirect_uri
// description: The URL where the user will be sent after authorization
// type: string
// - in: query
// name: setup_action
// description: The specific setup action callback identifier
// type: string
// responses:
// '200':
// description: Successfully authenticated
Expand All @@ -60,16 +66,35 @@ import (
// process a user logging in to Vela from
// the API or UI.
func GetAuthToken(c *gin.Context) {
var err error

// capture middleware values
tm := c.MustGet("token-manager").(*token.Manager)
m := c.MustGet("metadata").(*internal.Metadata)
l := c.MustGet("logger").(*logrus.Entry)

ctx := c.Request.Context()

// GitHub App and OAuth share the same callback URL,
// so we need to differentiate between the two using setup_action
if c.Request.FormValue("setup_action") == "install" {
redirect, err := api.GetAppInstallRedirectURL(ctx, l, m, c.Request.URL.Query())
if err != nil {
retErr := fmt.Errorf("unable to get app install redirect URL: %w", err)

util.HandleError(c, http.StatusBadRequest, retErr)

return
}

c.Redirect(http.StatusTemporaryRedirect, redirect)

return
}

// capture the OAuth state if present
oAuthState := c.Request.FormValue("state")

var err error

// capture the OAuth code if present
code := c.Request.FormValue("code")
if len(code) == 0 {
Expand Down
161 changes: 161 additions & 0 deletions api/install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// SPDX-License-Identifier: Apache-2.0

package api

import (
"context"
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"

"github.com/gin-gonic/gin"
"github.com/go-vela/server/api/types"
"github.com/go-vela/server/internal"
"github.com/go-vela/server/scm"
"github.com/go-vela/server/util"
"github.com/sirupsen/logrus"
)

// swagger:operation GET /install install Install
//
// Start SCM app installation flow and redirect to the external SCM destination
//
// ---
// produces:
// - application/json
// parameters:
// - in: query
// name: type
// description: The type of installation flow, either 'cli' or 'web'
// type: string
// - in: query
// name: port
// description: The local server port used during 'cli' flow
// type: string
// - in: query
// name: org_scm_id
// description: The SCM org id
// type: string
// - in: query
// name: repo_scm_id
// description: The SCM repo id
// type: string
// responses:
// '307':
// description: Redirected for installation
// '400':
// description: Invalid request payload
// schema:
// "$ref": "#/definitions/Error"
// '401':
// description: Unauthorized
// schema:
// "$ref": "#/definitions/Error"
// '503':
// description: Service unavailable
// schema:
// "$ref": "#/definitions/Error"

// Install represents the API handler to
// process an SCM app installation for Vela from
// the API or UI.
func Install(c *gin.Context) {
// capture middleware values
l := c.MustGet("logger").(*logrus.Entry)
scm := scm.FromContext(c)

l.Debug("redirecting to SCM to complete app flow installation")

orgSCMID, err := strconv.Atoi(util.FormParameter(c, "org_scm_id"))
if err != nil {
retErr := fmt.Errorf("unable to parse org_scm_id to integer: %v", err)

util.HandleError(c, http.StatusBadRequest, retErr)

return
}

repoSCMID, err := strconv.Atoi(util.FormParameter(c, "repo_scm_id"))
if err != nil {
retErr := fmt.Errorf("unable to parse repo_scm_id to integer: %v", err)

util.HandleError(c, http.StatusBadRequest, retErr)

return
}

// type cannot be empty
t := util.FormParameter(c, "type")
if len(t) == 0 {
retErr := errors.New("no type query provided")

util.HandleError(c, http.StatusBadRequest, retErr)

return
}

// port can be empty when using web flow
p := util.FormParameter(c, "port")

// capture query params
ri := &types.RepoInstall{
Type: t,
Port: p,
OrgSCMID: int64(orgSCMID),
RepoSCMID: int64(repoSCMID),
}

// construct the repo installation url
redirectURL, err := scm.GetRepoInstallURL(c.Request.Context(), ri)
if err != nil {
l.Errorf("unable to get repo install url: %v", err)

return
}

c.Redirect(http.StatusTemporaryRedirect, redirectURL)
}

// GetAppInstallRedirectURL is a helper function to generate the redirect URL for completing an app installation flow.
func GetAppInstallRedirectURL(ctx context.Context, l *logrus.Entry, m *internal.Metadata, q url.Values) (string, error) {
// extract state that is passed along during the installation process
pairs := strings.Split(q.Get("state"), ",")

values := make(map[string]string)

for _, pair := range pairs {
parts := strings.SplitN(pair, "=", 2)
if len(parts) == 2 {
key := strings.TrimSpace(parts[0])

value := strings.TrimSpace(parts[1])

values[key] = value
}
}

t, p := values["type"], values["port"]

// default redirect location if a user ended up here
// by providing an unsupported type
r := fmt.Sprintf("%s/install", m.Vela.Address)

switch t {
// cli auth flow
case "cli":
r = fmt.Sprintf("http://127.0.0.1:%s", p)
// web auth flow
case "web":
r = fmt.Sprintf("%s%s", m.Vela.WebAddress, m.Vela.WebOauthCallbackPath)
}

// append the code and state values
r = fmt.Sprintf("%s?%s", r, q.Encode())

l.Debug("redirecting for final app installation flow")

return r, nil
}
86 changes: 86 additions & 0 deletions api/repo/install_html_url.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// SPDX-License-Identifier: Apache-2.0

package repo

import (
"fmt"
"net/http"

"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"

"github.com/go-vela/server/internal"
"github.com/go-vela/server/router/middleware/repo"
"github.com/go-vela/server/router/middleware/user"
"github.com/go-vela/server/scm"
"github.com/go-vela/server/util"
)

// swagger:operation GET /api/v1/repos/{org}/{repo}/install/html_url repos GetInstallHTMLURL
//
// Repair a hook for a repository in Vela and the configured SCM
//
// ---
// produces:
// - application/json
// parameters:
// - in: path
// name: org
// description: Name of the organization
// required: true
// type: string
// - in: path
// name: repo
// description: Name of the repository
// required: true
// type: string
// security:
// - ApiKeyAuth: []
// responses:
// '200':
// description: Successfully constructed the repo installation HTML URL
// schema:
// type: string
// '401':
// description: Unauthorized
// schema:
// "$ref": "#/definitions/Error"
// '404':
// description: Not found
// schema:
// "$ref": "#/definitions/Error"
// '500':
// description: Unexpected server error
// schema:
// "$ref": "#/definitions/Error"

// GetInstallHTMLURL represents the API handler to retrieve the
// SCM installation HTML URL for a particular repo and Vela server.
func GetInstallHTMLURL(c *gin.Context) {
// capture middleware values
m := c.MustGet("metadata").(*internal.Metadata)
l := c.MustGet("logger").(*logrus.Entry)
u := user.Retrieve(c)
r := repo.Retrieve(c)
scm := scm.FromContext(c)

l.Debug("constructing repo install url")

ri, err := scm.GetRepoInstallInfo(c.Request.Context(), u, r.GetOrg(), r.GetName())
if err != nil {
retErr := fmt.Errorf("unable to get repo scm install info %s: %w", u.GetName(), err)

util.HandleError(c, http.StatusInternalServerError, retErr)

return
}

// todo: use url.values etc
appInstallURL := fmt.Sprintf(
"%s/install?org_scm_id=%d&repo_scm_id=%d",
m.Vela.Address,
ri.OrgSCMID, ri.RepoSCMID,
)

c.JSON(http.StatusOK, fmt.Sprintf("%s", appInstallURL))
}
10 changes: 10 additions & 0 deletions api/types/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ import (
"strings"
)

// RepoInstall is the configuration for installing a repo into the SCM.
//
// swagger:model RepoInstall
type RepoInstall struct {
Type string
Port string
OrgSCMID int64
RepoSCMID int64
}

// Repo is the API representation of a repo.
//
// swagger:model Repo
Expand Down
2 changes: 1 addition & 1 deletion cmd/vela-server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func server(c *cli.Context) error {
metadata.Vela.OpenIDIssuer = oidcIssuer
tm.Issuer = oidcIssuer

jitter := wait.Jitter(5*time.Second, 2.0)
jitter := wait.Jitter(0*time.Second, 2.0)

logrus.Infof("retrieving initial platform settings after %v delay", jitter)

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/go-vela/server

go 1.23.1
go 1.23.2

replace github.com/go-vela/types => ../types

Expand Down
1 change: 1 addition & 0 deletions router/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ func RepoHandlers(base *gin.RouterGroup) {
_repo.DELETE("", perm.MustAdmin(), repo.DeleteRepo)
_repo.PATCH("/repair", perm.MustAdmin(), repo.RepairRepo)
_repo.PATCH("/chown", perm.MustAdmin(), repo.ChownRepo)
_repo.GET("/install/html_url", repo.GetInstallHTMLURL)

// Build endpoints
// * Service endpoints
Expand Down
3 changes: 3 additions & 0 deletions router/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ func Load(options ...gin.HandlerFunc) *gin.Engine {
authenticate.POST("/token", auth.PostAuthToken)
}

// Repo installation endpoint (GitHub App)
r.GET("/install", api.Install)

// API endpoints
baseAPI := r.Group(base, claims.Establish(), user.Establish())
{
Expand Down
16 changes: 14 additions & 2 deletions scm/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,20 @@ func (c *client) newClientToken(ctx context.Context, token string) *github.Clien
return github
}

// helper function to return the GitHub App token.
func (c *client) newGithubAppToken(ctx context.Context, r *api.Repo) (*github.Client, error) {
// helper function to return the GitHub App client for authenticating as the GitHub App itself using the RoundTripper.
func (c *client) newGithubAppClient(ctx context.Context) (*github.Client, error) {
// todo: create transport using context to apply tracing
// create a github client based off the existing GitHub App configuration
client, err := github.NewClient(&http.Client{Transport: c.AppsTransport}).WithEnterpriseURLs(c.config.API, c.config.API)
if err != nil {
return nil, err
}

return client, nil
}

// helper function to return the GitHub App installation token.
func (c *client) newGithubAppInstallationToken(ctx context.Context, r *api.Repo) (*github.Client, error) {
// create a github client based off the existing GitHub App configuration
client, err := github.NewClient(&http.Client{Transport: c.AppsTransport}).WithEnterpriseURLs(c.config.API, c.config.API)
if err != nil {
Expand Down
Loading

0 comments on commit 37b30a1

Please sign in to comment.