Skip to content

Commit

Permalink
Set up GitHub oauth for rerun button
Browse files Browse the repository at this point in the history
  • Loading branch information
mirandachrist committed Jun 13, 2019
1 parent 6d46353 commit 0d363a9
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 41 deletions.
6 changes: 3 additions & 3 deletions prow/cmd/deck/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -453,12 +453,13 @@ func prodOnlyMain(cfg config.Getter, o options, mux *http.ServeMux) *http.ServeM
githubOAuthConfig.InitGitHubOAuthConfig(cookie)

goa := githuboauth.NewAgent(&githubOAuthConfig, logrus.WithField("client", "githuboauth"))
oauthClient := &oauth2.Config{
oauthClient := &githuboauth.RealOAuthClient{Config: &oauth2.Config{
ClientID: githubOAuthConfig.ClientID,
ClientSecret: githubOAuthConfig.ClientSecret,
RedirectURL: githubOAuthConfig.RedirectURL,
Scopes: githubOAuthConfig.Scopes,
Endpoint: github.Endpoint,
},
}

repoSet := make(map[string]bool)
Expand Down Expand Up @@ -1199,6 +1200,5 @@ func handleFavicon(staticFilesLocation string, cfg config.Getter) http.HandlerFu

func isValidatedGitOAuthConfig(githubOAuthConfig *config.GitHubOAuthConfig) bool {
return githubOAuthConfig.ClientID != "" && githubOAuthConfig.ClientSecret != "" &&
githubOAuthConfig.RedirectURL != "" &&
githubOAuthConfig.FinalRedirectURL != ""
githubOAuthConfig.RedirectURL != ""
}
8 changes: 5 additions & 3 deletions prow/cmd/deck/static/pr/pr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,8 +387,10 @@ function getFullPRContext(builds: Job[], contexts: Context[]): UnifiedContext[]
*/
function loadPrStatus(prData: UserData): void {
const tideQueries: TideQuery[] = [];
for (const query of tideData.TideQueries) {
tideQueries.push(new TideQuery(query));
if (tideData.TideQueries != null) {
for (const query of tideData.TideQueries) {
tideQueries.push(new TideQuery(query));
}
}

const container = document.querySelector("#pr-container")!;
Expand Down Expand Up @@ -1157,7 +1159,7 @@ function createPRCard(pr: PullRequest, builds: UnifiedContext[] = [], queries: P
* Redirect to initiate github login flow.
*/
function forceGitHubLogin(): void {
window.location.href = window.location.origin + "/github-login";
window.location.href = window.location.origin + "/github-login?dest=pr";
}

type VagueState = "succeeded" | "failed" | "pending" | "unknown";
Expand Down
43 changes: 33 additions & 10 deletions prow/cmd/deck/static/prow/prow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,24 @@ function handleUpKey(): void {
adjustScroll(previousSibling);
}

/**
* Gets cookie by its name.
*/
function getCookieByName(name: string): string {
if (!document.cookie) {
return "";
}
const cookies = decodeURIComponent(document.cookie).split(";");
for (const cookie of cookies) {
const c = cookie.trim();
const pref = name + "=";
if (c.indexOf(pref) === 0) {
return c.slice(pref.length);
}
}
return "";
}

window.onload = (): void => {
const topNavigator = document.getElementById("top-navigator")!;
let navigatorTimeOut: number | undefined;
Expand Down Expand Up @@ -651,18 +669,23 @@ function redraw(fz: FuzzySearch): void {
}

function createRerunCell(modal: HTMLElement, rerunElement: HTMLElement, prowjob: string): HTMLTableDataCellElement {
const url = `https://${window.location.hostname}/rerun?prowjob=${prowjob}`;
const url = `http://${window.location.hostname}/rerun?prowjob=${prowjob}`;
const c = document.createElement("td");
const i = icon.create("refresh", "Show instructions for rerunning this job");
i.onclick = () => {
modal.style.display = "block";
rerunElement.innerHTML = `kubectl create -f "<a href="${url}">${url}</a>"`;
const copyButton = document.createElement('a');
copyButton.className = "mdl-button mdl-js-button mdl-button--icon";
copyButton.onclick = () => copyToClipboardWithToast(`kubectl create -f "${url}"`);
copyButton.innerHTML = "<i class='material-icons state triggered' style='color: gray'>file_copy</i>";
rerunElement.appendChild(copyButton);
};
const login = getCookieByName("github_login");
if (login === "") {
i.href = `http://${window.location.hostname}/github-login`;
} else {
i.onclick = () => {
modal.style.display = "block";
rerunElement.innerHTML = `kubectl create -f "<a href="${url}">${url}</a>"`;
const copyButton = document.createElement('a');
copyButton.className = "mdl-button mdl-js-button mdl-button--icon";
copyButton.onclick = () => copyToClipboardWithToast(`kubectl create -f "${url}" aldaslkfjalskfj`);
copyButton.innerHTML = "<i class='material-icons state triggered' style='color: gray'>file_copy</i>";
rerunElement.appendChild(copyButton);
};
}
c.appendChild(i);
c.classList.add("icon-cell");
return c;
Expand Down
9 changes: 4 additions & 5 deletions prow/config/githuboauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,10 @@ type Cookie struct {
// GitHubOAuthConfig is a config for requesting users access tokens from GitHub API. It also has
// a Cookie Store that retains user credentials deriving from GitHub API.
type GitHubOAuthConfig struct {
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
RedirectURL string `json:"redirect_url"`
Scopes []string `json:"scopes,omitempty"`
FinalRedirectURL string `json:"final_redirect_url"`
ClientID string `json:"client_id"`
ClientSecret string `json:"client_secret"`
RedirectURL string `json:"redirect_url"`
Scopes []string `json:"scopes,omitempty"`

CookieStore *sessions.CookieStore `json:"-"`
}
Expand Down
37 changes: 33 additions & 4 deletions prow/githuboauth/githuboauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,30 @@ type GitHubClientGetter interface {

// OAuthClient is an interface for a GitHub OAuth client.
type OAuthClient interface {
GetConfig() *oauth2.Config
// Exchanges code from GitHub OAuth redirect for user access token.
Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error)
// Returns a URL to GitHub's OAuth 2.0 consent page. The state is a token to protect the user
// from an XSRF attack.
AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string
}

type RealOAuthClient struct {
Config *oauth2.Config
}

func (client RealOAuthClient) GetConfig() *oauth2.Config {
return client.Config
}

func (client RealOAuthClient) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
return client.Config.Exchange(ctx, code, opts...)
}

func (client RealOAuthClient) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
return client.Config.AuthCodeURL(state, opts...)
}

type githubClientGetter struct{}

func (gci *githubClientGetter) GetGitHubClient(accessToken string, dryRun bool) GitHubClientWrapper {
Expand Down Expand Up @@ -92,6 +109,7 @@ func NewAgent(config *config.GitHubOAuthConfig, logger *logrus.Entry) *Agent {
// redirect user to GitHub OAuth end-point for authentication.
func (ga *Agent) HandleLogin(client OAuthClient) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
redirectPage := r.URL.Query().Get("dest")
stateToken := xsrftoken.Generate(ga.gc.ClientSecret, "", "")
state := hex.EncodeToString([]byte(stateToken))
oauthSession, err := ga.gc.CookieStore.New(r, oauthSessionCookie)
Expand All @@ -109,7 +127,16 @@ func (ga *Agent) HandleLogin(client OAuthClient) http.HandlerFunc {
return
}

redirectURL := client.AuthCodeURL(state, oauth2.ApprovalForce, oauth2.AccessTypeOnline)
newClient := &RealOAuthClient{
&oauth2.Config{
ClientID: client.GetConfig().ClientID,
ClientSecret: client.GetConfig().ClientSecret,
RedirectURL: client.GetConfig().RedirectURL + "?dest=" + redirectPage,
Scopes: client.GetConfig().Scopes,
Endpoint: client.GetConfig().Endpoint,
},
}
redirectURL := newClient.AuthCodeURL(state, oauth2.ApprovalForce, oauth2.AccessTypeOnline)
http.Redirect(w, r, redirectURL, http.StatusFound)
}
}
Expand All @@ -135,7 +162,7 @@ func (ga *Agent) HandleLogout(client OAuthClient) http.HandlerFunc {
loginCookie.Expires = time.Now().Add(-time.Hour * 24)
http.SetCookie(w, loginCookie)
}
http.Redirect(w, r, ga.gc.FinalRedirectURL, http.StatusFound)
http.Redirect(w, r, r.URL.Host, http.StatusFound)
}
}

Expand All @@ -144,6 +171,8 @@ func (ga *Agent) HandleLogout(client OAuthClient) http.HandlerFunc {
// the final destination in the config, which should be the front-end.
func (ga *Agent) HandleRedirect(client OAuthClient, getter GitHubClientGetter) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
redirectPage := r.URL.Query().Get("dest")
finalRedirectURL := r.URL.Host + "/" + redirectPage
state := r.FormValue("state")
stateTokenRaw, err := hex.DecodeString(state)
if err != nil {
Expand All @@ -163,7 +192,7 @@ func (ga *Agent) HandleRedirect(client OAuthClient, getter GitHubClientGetter) h
}
secretState, ok := oauthSession.Values[stateKey].(string)
if !ok {
ga.serverError(w, "Get secret state", fmt.Errorf("empty string or cannot convert to string"))
ga.serverError(w, "Get secret state", fmt.Errorf("empty string or cannot convert to string: %v", oauthSession.Values))
return
}
// Validate the state parameter to prevent cross-site attack.
Expand Down Expand Up @@ -219,7 +248,7 @@ func (ga *Agent) HandleRedirect(client OAuthClient, getter GitHubClientGetter) h
Expires: time.Now().Add(time.Hour * 24 * 30),
Secure: true,
})
http.Redirect(w, r, ga.gc.FinalRedirectURL, http.StatusFound)
http.Redirect(w, r, finalRedirectURL, http.StatusFound)
}
}

Expand Down
53 changes: 37 additions & 16 deletions prow/githuboauth/githuboauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"encoding/hex"
"net/http"
"net/http/httptest"
"strings"
"testing"

"github.com/google/go-github/github"
Expand All @@ -36,30 +37,34 @@ import (

const mockAccessToken = "justSomeRandomSecretToken"

type MockOAuthClient struct{}
type MockOAuthClient struct {
}

func (c MockOAuthClient) GetConfig() *oauth2.Config {
return &oauth2.Config{RedirectURL: "www.something.com/"}
}

func (c *MockOAuthClient) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
func (c MockOAuthClient) Exchange(ctx context.Context, code string, opts ...oauth2.AuthCodeOption) (*oauth2.Token, error) {
return &oauth2.Token{
AccessToken: mockAccessToken,
}, nil
}

func (c *MockOAuthClient) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
func (c MockOAuthClient) AuthCodeURL(state string, opts ...oauth2.AuthCodeOption) string {
return "mock-auth-url"
}

func getMockConfig(cookie *sessions.CookieStore) *config.GitHubOAuthConfig {
clientID := "mock-client-id"
clientSecret := "mock-client-secret"
redirectURL := "/uni-test/redirect-url"
redirectURL := "uni-test/redirect-url"
scopes := []string{}

return &config.GitHubOAuthConfig{
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: redirectURL,
Scopes: scopes,
FinalRedirectURL: "/unit-test/final-redirect-url",
ClientID: clientID,
ClientSecret: clientSecret,
RedirectURL: redirectURL,
Scopes: scopes,

CookieStore: cookie,
}
Expand All @@ -80,13 +85,14 @@ func isEqual(token1 *oauth2.Token, token2 *oauth2.Token) bool {
}

func TestHandleLogin(t *testing.T) {
dest := "wherever"
cookie := sessions.NewCookieStore([]byte("secret-key"))
mockConfig := getMockConfig(cookie)
mockLogger := logrus.WithField("uni-test", "githuboauth")
mockAgent := NewAgent(mockConfig, mockLogger)
mockOAuthClient := &MockOAuthClient{}
mockOAuthClient := MockOAuthClient{}

mockRequest := httptest.NewRequest(http.MethodGet, "/mock-login", nil)
mockRequest := httptest.NewRequest(http.MethodGet, "/mock-login?dest="+dest, nil)
mockResponse := httptest.NewRecorder()

handleLoginFn := mockAgent.HandleLogin(mockOAuthClient)
Expand Down Expand Up @@ -125,14 +131,24 @@ func TestHandleLogin(t *testing.T) {
if state == "" {
t.Error("Expect state parameter is not empty, found empty")
}
destCount := 0
path := mockResponse.Header().Get("Location")
for _, q := range strings.Split(path, "&") {
if q == "redirect_uri=www.something.com%2F%3Fdest%3Dwherever" {
destCount += 1
}
}
if destCount != 1 {
t.Errorf("Redirect URI in path does not include correct destination. path: %s, destination: %s", path, dest)
}
}

func TestHandleLogout(t *testing.T) {
cookie := sessions.NewCookieStore([]byte("secret-key"))
mockConfig := getMockConfig(cookie)
mockLogger := logrus.WithField("uni-test", "githuboauth")
mockAgent := NewAgent(mockConfig, mockLogger)
mockOAuthClient := &MockOAuthClient{}
mockOAuthClient := MockOAuthClient{}

mockRequest := httptest.NewRequest(http.MethodGet, "/mock-logout", nil)
_, err := cookie.New(mockRequest, tokenSession)
Expand Down Expand Up @@ -172,7 +188,7 @@ func TestHandleLogoutWithLoginSession(t *testing.T) {
mockConfig := getMockConfig(cookie)
mockLogger := logrus.WithField("uni-test", "githuboauth")
mockAgent := NewAgent(mockConfig, mockLogger)
mockOAuthClient := &MockOAuthClient{}
mockOAuthClient := MockOAuthClient{}

mockRequest := httptest.NewRequest(http.MethodGet, "/mock-logout", nil)
_, err := cookie.New(mockRequest, tokenSession)
Expand Down Expand Up @@ -232,7 +248,7 @@ func TestHandleRedirectWithInvalidState(t *testing.T) {
mockConfig := getMockConfig(cookie)
mockLogger := logrus.WithField("uni-test", "githuboauth")
mockAgent := NewAgent(mockConfig, mockLogger)
mockOAuthClient := &MockOAuthClient{}
mockOAuthClient := MockOAuthClient{}
mockStateToken := createMockStateToken(mockConfig)

mockRequest := httptest.NewRequest(http.MethodGet, "/mock-login", nil)
Expand Down Expand Up @@ -262,10 +278,11 @@ func TestHandleRedirectWithValidState(t *testing.T) {
mockLogger := logrus.WithField("uni-test", "githuboauth")
mockAgent := NewAgent(mockConfig, mockLogger)
mockLogin := "foo_name"
mockOAuthClient := &MockOAuthClient{}
mockOAuthClient := MockOAuthClient{}
mockStateToken := createMockStateToken(mockConfig)

mockRequest := httptest.NewRequest(http.MethodGet, "/mock-login", nil)
dest := "somewhere"
mockRequest := httptest.NewRequest(http.MethodGet, "/mock-login?dest="+dest, nil)
mockResponse := httptest.NewRecorder()
query := mockRequest.URL.Query()
query.Add("state", mockStateToken)
Expand Down Expand Up @@ -321,4 +338,8 @@ func TestHandleRedirectWithValidState(t *testing.T) {
if loginCookie.Value != mockLogin {
t.Errorf("Mismatch github login. Got %v, expected %v", loginCookie.Value, mockLogin)
}
path := mockResponse.Header().Get("Location")
if path != "/"+dest {
t.Errorf("Incorrect final redirect URL. Actual path: %s, Expected path: /%s", path, dest)
}
}

0 comments on commit 0d363a9

Please sign in to comment.