From c0a1a570378f79ca9e2ef767751408725151aa32 Mon Sep 17 00:00:00 2001 From: davidmarne Date: Sat, 18 Mar 2017 10:20:27 -0600 Subject: [PATCH] gha - initial data endpoint and github auth --- Dockerfile | 1 + glide.lock | 29 ++++--- glide.yaml | 4 + server/game.go | 44 +++++----- server/githubauth.go | 140 ++++++++++++++++++++++++++++++ server/{auth.go => googleauth.go} | 27 +++++- server/initialdata.go | 93 ++++++++++++++++++++ server/main.go | 41 ++++++--- server/map.go | 4 + server/utils.go | 49 ++++------- 10 files changed, 353 insertions(+), 79 deletions(-) create mode 100644 server/githubauth.go rename server/{auth.go => googleauth.go} (86%) create mode 100644 server/initialdata.go diff --git a/Dockerfile b/Dockerfile index fc13c4f..d4ba30a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,5 @@ FROM alpine +RUN apk --update add ca-certificates RUN mkdir /server COPY server/server /server/server diff --git a/glide.lock b/glide.lock index cd43780..3d9ff8e 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: 6e45dafdcd608b6ac00bf7f86fc8cda6456af740e9585c5feecff636ce70abbf -updated: 2016-10-16T12:00:11.450804289-06:00 +hash: 49bee0ff4c7503a4b6dd43549d5a18e6e344da45796478a754160ff3c49d46c9 +updated: 2016-10-29T14:53:33.691891918-06:00 imports: - name: cloud.google.com/go version: 3b1ae45394a234c385be014e9a488f2bb6eef821 @@ -9,23 +9,23 @@ imports: - compute/metadata - internal - name: git.apache.org/thrift.git - version: af81cf0c6180cda4791e023a37ad134247fa7794 + version: 59cb6661bcee265d39ad524154472ebe27760f1e subpackages: - lib/go/thrift - name: github.com/blang/semver version: 31b736133b98f26d5e078ec9eb591666edfd091f - name: github.com/codegp/cloud-persister - version: f32b964c7d7bb389701be9d4c769b7ff9e71014a + version: 23b8263a4676fa593cefd96e8ce6b8cfa682d53a subpackages: - models - name: github.com/codegp/env - version: 336f55c547ee75bb1941c9e5bf4dda6586332224 + version: 995611706bf3d9a4cff43cd004fd44919493995f - name: github.com/codegp/game-object-types - version: f4be21842bbbce5f419ba44aada8778723e8205b + version: 2a3c7fd5f280683c6c4e56f04ddbe202c4ccdb6e subpackages: - types - name: github.com/codegp/kube-client - version: 7dc023beb154d29c246e75bc182d920b3d875519 + version: 06e43dbcf8f06134b00e0433e1d15b5947dc875a - name: github.com/coreos/go-oidc version: 5644a2f50e2d2d5ba0b474bc5bc55fea1925936d subpackages: @@ -148,6 +148,14 @@ imports: - utils/tail - validate - version +- name: github.com/google/go-github + version: f7fcf6f52ff94adf1cc0ded41e7768d2ad729972 + subpackages: + - github +- name: github.com/google/go-querystring + version: 9235644dd9e52eeae6fa48efd539fdc351a0af53 + subpackages: + - query - name: github.com/google/gofuzz version: bbcb9da2d746f8bdbd6a936686a0a6067ada0ec5 - name: github.com/gorilla/context @@ -199,14 +207,15 @@ imports: - internal/timeseries - idna - name: golang.org/x/oauth2 - version: 1e695b1c8febf17aad3bfa7bf0a819ef94b98ad5 + version: 25b4fb1468cb89700c7c060cb99f30581a61f5e3 subpackages: - google + - github - internal - jws - jwt - name: golang.org/x/sys - version: 9bb9f0998d48b31547d975974935ae9b48c7a03c + version: c200b10b5d5e122be351b67af224adc6128af5bf subpackages: - unix - name: golang.org/x/text @@ -275,7 +284,7 @@ imports: - name: gopkg.in/yaml.v2 version: 53feefa2559fb8dfa8d81baad31be332c97d6c77 - name: k8s.io/kubernetes - version: 712d3d2cd3abe3bf862f813c0969f04d2454afb2 + version: 8b6cebcb3532413085252f6148629f6c66de3adb subpackages: - pkg/api - pkg/api/unversioned diff --git a/glide.yaml b/glide.yaml index 4e35b95..eb0aa7e 100644 --- a/glide.yaml +++ b/glide.yaml @@ -19,6 +19,10 @@ import: - package: golang.org/x/oauth2 subpackages: - google + - github - package: google.golang.org/api subpackages: - plus/v1 +- package: github.com/google/go-github + subpackages: + - github diff --git a/server/game.go b/server/game.go index 4b12f07..968e409 100644 --- a/server/game.go +++ b/server/game.go @@ -24,28 +24,21 @@ func GetGame(w http.ResponseWriter, r *http.Request) *requestError { } func PostGame(w http.ResponseWriter, r *http.Request) *requestError { - projectID, rerr := readIDFromRequest(r, "projectID") - if rerr != nil { - return rerr - } - - proj, err := cp.GetProject(projectID) + defer r.Body.Close() + body, err := ioutil.ReadAll(r.Body) if err != nil { - return requestErrorf(err, "Error getting project from datastore") - } - - mapID, rerr := readIDFromRequest(r, "mapID") - if rerr != nil { - return rerr + return requestErrorf(err, "Error unmarshalling itemType") } // TODO: if gametype.numteams > 1 find competitors game := &models.Game{ - MapID: mapID, - ProjectIDs: []int64{proj.ID}, - Created: time.Now(), - GameTypeID: proj.GameTypeID, - Complete: false, + Created: time.Now(), + Complete: false, + } + + err = json.Unmarshal(body, game) + if err != nil { + return requestErrorf(err, "Error unmarshalling game") } game, err = cp.AddGame(game) @@ -53,14 +46,23 @@ func PostGame(w http.ResponseWriter, r *http.Request) *requestError { return requestErrorf(err, "Error adding game to datastore") } - _, err = kc.StartGame(game) + projects, err := cp.GetMultiProject(game.ProjectIDs) + if err != nil { + return requestErrorf(err, "Error getting projects from datastore") + } + + _, err = kc.StartGame(game, projects) if err != nil { return requestErrorf(err, "Error starting game pod") } - proj.GameIDs = append(proj.GameIDs, game.ID) - if cp.UpdateProject(proj) != nil { - return requestErrorf(err, "Error updating project") + for _, proj := range projects { + proj.GameIDs = append(proj.GameIDs, game.ID) + + // TODO: put multi + if cp.UpdateProject(proj) != nil { + return requestErrorf(err, "Error updating project") + } } return marshalAndWriteResponse(w, game) diff --git a/server/githubauth.go b/server/githubauth.go new file mode 100644 index 0000000..cc59f69 --- /dev/null +++ b/server/githubauth.go @@ -0,0 +1,140 @@ +package main + +import ( + "encoding/gob" + "log" + "net/http" + "os" + + "github.com/google/go-github/github" + "golang.org/x/oauth2" + githuboauth "golang.org/x/oauth2/github" + + "golang.org/x/net/context" + + "github.com/satori/go.uuid" +) + +const githubProfileSessionKey = "github_user" + +func init() { + // Gob encoding for gorilla/sessions + gob.Register(&github.User{}) +} + +func handleGitHubLogin(w http.ResponseWriter, r *http.Request) *requestError { + sessionID := uuid.NewV4().String() + + oauthFlowSession, err := sessionStore.New(r, sessionID) + log.Printf("CRETATE SESS %v", oauthFlowSession) + if err != nil { + return requestErrorf(err, "could not create oauth session: %v", err) + } + oauthFlowSession.Options.MaxAge = 10 * 60 // 10 minutes + + // redirectURL, err := validateRedirectURL(r.FormValue("redirect")) + // if err != nil { + // return requestErrorf(err, "invalid redirect URL: %v", err) + // } + oauthFlowSession.Values[oauthFlowRedirectKey] = "http://localhost:3000/" + + if err := oauthFlowSession.Save(r, w); err != nil { + return requestErrorf(err, "could not save session: %v", err) + } + + url := githubOAuthConfig.AuthCodeURL(sessionID, oauth2.AccessTypeOnline) + http.Redirect(w, r, url, http.StatusFound) + return nil +} + +func handleGitHubCallback(w http.ResponseWriter, r *http.Request) *requestError { + oauthFlowSession, err := sessionStore.Get(r, r.FormValue("state")) + if err != nil { + return requestErrorf(err, "invalid state parameter. try logging in again.") + } + + redirectURL, ok := oauthFlowSession.Values[oauthFlowRedirectKey].(string) + log.Println("redirectURL", redirectURL) + // Validate this callback request came from the app. + if !ok { + return requestErrorf(err, "invalid state parameter. try logging in again.") + } + + code := r.FormValue("code") + token, err := githubOAuthConfig.Exchange(context.Background(), code) + if err != nil { + return requestErrorf(err, "could not get auth token: %v", err) + } + + session, err := sessionStore.New(r, defaultSessionID) + if err != nil { + return requestErrorf(err, "could not get default session: %v", err) + } + + user, err := fetchGithubUser(context.Background(), token) + if err != nil { + return requestErrorf(err, "fetchGithubUser failed: %v", err) + } + + session.Values[oauthTokenSessionKey] = token + session.Values[githubProfileSessionKey] = user + + logger.Infof("token %v \n\nsession %v", token, session) + if err := session.Save(r, w); err != nil { + return requestErrorf(err, "could not save session: %v", err) + } + + logger.Infof("Logged in as GitHub user: %s\n", *user.Login) + http.Redirect(w, r, redirectURL, http.StatusFound) + return nil +} + +func fetchGithubUser(ctx context.Context, tok *oauth2.Token) (*github.User, error) { + oauthClient := oauth2.NewClient(ctx, githubOAuthConfig.TokenSource(ctx, tok)) + client := github.NewClient(oauthClient) + user, _, err := client.Users.Get("") + if err != nil { + return nil, err + } + return user, nil +} + +func configureGithubOAuthClient() *oauth2.Config { + redirectURL := os.Getenv("GITHUB_OAUTH2_CALLBACK") + clientID := os.Getenv("GITHUB_CLIENT_ID") + clientSecret := os.Getenv("GITHUB_CLIENT_SECRET") + + if redirectURL == "" || clientID == "" || clientSecret == "" { + logger.Fatal("OAuth2 environment variables not found!") + } + return &oauth2.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + RedirectURL: redirectURL, + // select level of access you want https://developer.github.com/v3/oauth/#scopes + Scopes: []string{"user:email"}, + Endpoint: githuboauth.Endpoint, + } +} + +// profileFromSession retreives the Google+ profile from the default session. +// Returns nil if the profile cannot be retreived (e.g. user is logged out). +func githubUserFromSession(r *http.Request) *github.User { + session, err := sessionStore.Get(r, defaultSessionID) + log.Printf("sess %v %v", session, err) + if err != nil { + return nil + } + tok, ok := session.Values[oauthTokenSessionKey].(*oauth2.Token) + log.Printf("tok %v %v", tok, ok) + if !ok || !tok.Valid() { + return nil + } + + profile, ok := session.Values[githubProfileSessionKey].(*github.User) + log.Printf("profile %v %v", profile, ok) + if !ok { + return nil + } + return profile +} diff --git a/server/auth.go b/server/googleauth.go similarity index 86% rename from server/auth.go rename to server/googleauth.go index d6c6e29..2b270d6 100644 --- a/server/auth.go +++ b/server/googleauth.go @@ -5,9 +5,11 @@ import ( "errors" "net/http" "net/url" + "os" "golang.org/x/net/context" "golang.org/x/oauth2" + "golang.org/x/oauth2/google" "github.com/satori/go.uuid" "google.golang.org/api/plus/v1" @@ -55,7 +57,7 @@ func loginHandler(w http.ResponseWriter, r *http.Request) *requestError { // Use the session ID for the "state" parameter. // This protects against CSRF (cross-site request forgery). // See https://godoc.org/golang.org/x/oauth2#Config.AuthCodeURL for more detail. - url := oAuthConfig.AuthCodeURL(sessionID, oauth2.ApprovalForce, + url := googleOAuthConfig.AuthCodeURL(sessionID, oauth2.ApprovalForce, oauth2.AccessTypeOnline) http.Redirect(w, r, url, http.StatusFound) return nil @@ -96,7 +98,7 @@ func oauthCallbackHandler(w http.ResponseWriter, r *http.Request) *requestError } code := r.FormValue("code") - tok, err := oAuthConfig.Exchange(context.Background(), code) + tok, err := googleOAuthConfig.Exchange(context.Background(), code) if err != nil { return requestErrorf(err, "could not get auth token: %v", err) } @@ -119,6 +121,7 @@ func oauthCallbackHandler(w http.ResponseWriter, r *http.Request) *requestError return requestErrorf(err, "could not save session: %v", err) } + logger.Infof("Logged in as google user: %v\n", profile) http.Redirect(w, r, redirectURL, http.StatusFound) return nil } @@ -126,7 +129,7 @@ func oauthCallbackHandler(w http.ResponseWriter, r *http.Request) *requestError // fetchProfile retrieves the Google+ profile of the user associated with the // provided OAuth token. func fetchProfile(ctx context.Context, tok *oauth2.Token) (*plus.Person, error) { - client := oauth2.NewClient(ctx, oAuthConfig.TokenSource(ctx, tok)) + client := oauth2.NewClient(ctx, googleOAuthConfig.TokenSource(ctx, tok)) plusService, err := plus.New(client) if err != nil { return nil, err @@ -169,7 +172,6 @@ func profileFromSession(r *http.Request) *plus.Person { } return profile } - // stripProfile returns a subset of a plus.Person. func stripProfile(p *plus.Person) *plus.Person { return &plus.Person{ @@ -181,3 +183,20 @@ func stripProfile(p *plus.Person) *plus.Person { Url: p.Url, } } + +func configureGoogleOAuthClient() *oauth2.Config { + redirectURL := os.Getenv("GOOGLE_OAUTH2_CALLBACK") + clientID := os.Getenv("GOOGLE_CLIENT_ID") + clientSecret := os.Getenv("GOOGLE_CLIENT_SECRET") + + if redirectURL == "" || clientID == "" || clientSecret == "" { + logger.Fatal("OAuth2 environment variables not found!") + } + return &oauth2.Config{ + ClientID: clientID, + ClientSecret: clientSecret, + RedirectURL: redirectURL, + Scopes: []string{"email", "profile"}, + Endpoint: google.Endpoint, + } +} diff --git a/server/initialdata.go b/server/initialdata.go new file mode 100644 index 0000000..555a8cc --- /dev/null +++ b/server/initialdata.go @@ -0,0 +1,93 @@ +package main + +import ( + "net/http" + + "github.com/codegp/cloud-persister/models" + + "github.com/codegp/game-object-types/types" +) + +func GetInitialData(w http.ResponseWriter, r *http.Request) *requestError { + user, err := getUserFromContext(r) + if err != nil { + return requestErrorf(err, "Error getting user from session") + } + + projects, err := cp.GetMultiProject(user.ProjectIDs) + if err != nil { + return requestErrorf(err, "Error getting projects for user") + } + + gameTypes, err := cp.ListGameTypes() + if err != nil { + return requestErrorf(err, "Error querying gameTypes from datastore") + } + + gameTypeCache := map[int64]*models.GameType{} + var maps []*models.Map + for _, proj := range projects { + gameType, found := gameTypeCache[proj.GameTypeID] + if !found { + gameType, err = cp.GetGameType(proj.GameTypeID) + if err != nil { + return requestErrorf(err, "Error getting gametype for project") + } + + gameTypeCache[proj.GameTypeID] = gameType + } + + pmaps, err := cp.GetMultiMap(gameType.MapIDs) + if err != nil { + return requestErrorf(err, "Error getting maps for project") + } + maps = append(maps, pmaps...) + } + + botTypes, err := cp.ListBotTypes() + if err != nil { + return requestErrorf(err, "Error getting botTypes from datastore") + } + + attackTypes, err := cp.ListAttackTypes() + if err != nil { + return requestErrorf(err, "Error getting attackTypes from datastore") + } + + moveTypes, err := cp.ListMoveTypes() + if err != nil { + return requestErrorf(err, "Error getting moveTypes from datastore") + } + + itemTypes, err := cp.ListItemTypes() + if err != nil { + return requestErrorf(err, "Error getting itemTypes from datastore") + } + + terrainTypes, err := cp.ListTerrainTypes() + if err != nil { + return requestErrorf(err, "Error getting terrainTypes from datastore") + } + + return marshalAndWriteResponse(w, &InitialData{ + Maps: maps, + Projects: projects, + GameTypes: gameTypes, + BotTypes: botTypes, + AttackTypes: attackTypes, + ItemTypes: itemTypes, + TerrainTypes: terrainTypes, + MoveTypes: moveTypes, + }) +} + +type InitialData struct { + Maps []*models.Map + Projects []*models.Project + GameTypes []*models.GameType + BotTypes []*types.BotType + AttackTypes []*types.AttackType + ItemTypes []*types.ItemType + TerrainTypes []*types.TerrainType + MoveTypes []*types.MoveType +} diff --git a/server/main.go b/server/main.go index 6e9af51..c843aaf 100644 --- a/server/main.go +++ b/server/main.go @@ -3,6 +3,7 @@ package main import ( "net/http" "os" + "time" "github.com/Sirupsen/logrus" "github.com/codegp/cloud-persister" @@ -11,17 +12,18 @@ import ( "github.com/gorilla/sessions" "github.com/codegp/env" - "github.com/codegp/kube-client" + orch "github.com/codegp/job-client" + "github.com/codegp/job-client/jobclient" "golang.org/x/oauth2" ) var ( - cp *cloudpersister.CloudPersister - kc *kubeclient.KubeClient - isLocal bool - oAuthConfig *oauth2.Config - sessionStore sessions.Store - logger *logrus.Logger + cp *cloudpersister.CloudPersister + kc jobclient.JobClient + googleOAuthConfig *oauth2.Config + githubOAuthConfig *oauth2.Config + sessionStore sessions.Store + logger *logrus.Logger ) func init() { @@ -29,18 +31,25 @@ func init() { logger = logrus.New() logger.Out = os.Stderr logger.Infof("ENV %s %v %s", env.GCloudProjectID(), env.IsLocal(), os.Getenv("DATASTORE_EMULATOR_HOST")) - cp, err = cloudpersister.NewCloudPersister() - if err != nil { - logger.Fatalf("Failed to start cloud persister: %v", err) + + for { + cp, err = cloudpersister.NewCloudPersister() + if err == nil || !env.IsLocal() { + break + } + + logger.Errorf("Failed to create cloud persister. Confirm DSEmulator is running.\nError: %v", err) + time.Sleep(time.Millisecond * time.Duration(1000)) } - kc, err = kubeclient.NewClient() + kc, err = orch.GetJobClient() if err != nil { - logger.Fatalf("Failed to start kube client: %v", err) + logger.Fatalf("Failed to start mikube client: %v", err) } if !env.IsLocal() { - oAuthConfig = configureOAuthClient() + googleOAuthConfig = configureGoogleOAuthClient() + githubOAuthConfig = configureGithubOAuthClient() // TODO get better secret cookieStore := sessions.NewCookieStore([]byte("something-very-secret")) cookieStore.Options = &sessions.Options{ @@ -55,6 +64,7 @@ func main() { apiRouter := baseRouter.PathPrefix("/console").Subrouter() apiRouter.HandleFunc("/user", sessionMiddleware(GetUser)).Methods("GET") + apiRouter.HandleFunc("/initialData", sessionMiddleware(GetInitialData)).Methods("GET") apiRouter.HandleFunc("/project/{projectID}", sessionMiddleware(GetProject)).Methods("GET") apiRouter.HandleFunc("/project", sessionMiddleware(PostProject)).Methods("POST") @@ -104,6 +114,11 @@ func main() { baseRouter.HandleFunc("/login", errorMiddleware(loginHandler)).Methods("GET") baseRouter.HandleFunc("/logout", errorMiddleware(logoutHandler)).Methods("POST") baseRouter.HandleFunc("/oauth2callback", errorMiddleware(oauthCallbackHandler)).Methods("GET") + + baseRouter.HandleFunc("/githublogin", errorMiddleware(handleGitHubLogin)).Methods("GET") + // baseRouter.HandleFunc("/githublogout", errorMiddleware(logoutHandler)).Methods("POST") + baseRouter.HandleFunc("/githuboauth2callback", errorMiddleware(handleGitHubCallback)).Methods("GET") + // baseRouter.HandleFunc("/{rest:.*}", ServeDistFile) http.Handle("/", &AppServer{baseRouter}) diff --git a/server/map.go b/server/map.go index 18cb36e..9cd5d22 100644 --- a/server/map.go +++ b/server/map.go @@ -32,6 +32,10 @@ func PostMap(w http.ResponseWriter, r *http.Request) *requestError { } gameType.MapIDs = append(gameType.MapIDs, m.ID) + err = cp.UpdateGameType(gameType) + if err != nil { + return requestErrorf(err, "Error adding map id to gametype") + } defer r.Body.Close() body, err := ioutil.ReadAll(r.Body) diff --git a/server/utils.go b/server/utils.go index 764c382..920e26a 100644 --- a/server/utils.go +++ b/server/utils.go @@ -4,14 +4,11 @@ import ( "encoding/json" "fmt" "net/http" - "os" "strconv" "github.com/codegp/cloud-persister/models" "github.com/codegp/env" "github.com/gorilla/mux" - "golang.org/x/oauth2" - "golang.org/x/oauth2/google" ) type requestHandler func(http.ResponseWriter, *http.Request) *requestError @@ -31,7 +28,7 @@ func requestErrorf(err error, format string, v ...interface{}) *requestError { func sessionMiddleware(h requestHandler) func(http.ResponseWriter, *http.Request) { return (func(w http.ResponseWriter, r *http.Request) { - if !env.IsLocal() && profileFromSession(r) == nil { + if !env.IsLocal() && profileFromSession(r) == nil && githubUserFromSession(r) == nil { http.Error(w, "Please go to the splash page and login", http.StatusUnauthorized) return } @@ -75,42 +72,32 @@ func marshalAndWriteResponse(w http.ResponseWriter, toMarshal interface{}) *requ return nil } -func configureOAuthClient() *oauth2.Config { - redirectURL := os.Getenv("OAUTH2_CALLBACK") - clientID := os.Getenv("CLIENT_ID") - clientSecret := os.Getenv("CLIENT_SECRET") - - if redirectURL == "" || clientID == "" || clientSecret == "" { - logger.Fatal("OAuth2 environment variables not found!") - } - return &oauth2.Config{ - ClientID: clientID, - ClientSecret: clientSecret, - RedirectURL: redirectURL, - Scopes: []string{"email", "profile"}, - Endpoint: google.Endpoint, - } -} - func getUserFromContext(r *http.Request) (*models.User, error) { - var ID int64 - var err error if env.IsLocal() { // if local use a generic user - ID = 12345 - } else { - // get the profile info from the users session - profile := profileFromSession(r) - if profile == nil { - return nil, fmt.Errorf("No profile found in session") - } + return getUserByID(12345) + } - ID, err = strconv.ParseInt(profile.Id[2:], 10, 64) + // get the profile info from the users session + profile := profileFromSession(r) + if profile != nil { + ID, err := strconv.ParseInt(profile.Id[2:], 10, 64) if err != nil { return nil, fmt.Errorf("Error parsing google+ profile ID: %v", err) } + + return getUserByID(ID) + } + + ghUser := githubUserFromSession(r) + if ghUser != nil { + return getUserByID(int64(*ghUser.ID)) } + return nil, fmt.Errorf("No profiles found in session") +} + +func getUserByID(ID int64) (*models.User, error) { user, err := cp.GetUser(ID) logger.Infof("user %v\nerr %v", user, err) if err != nil {