package main import ( "bytes" "context" "encoding/hex" "encoding/json" "flag" "fmt" "io/ioutil" "log" "net/http" "net/url" "os" "time" "golang.org/x/oauth2" "github.com/gin-gonic/gin" ) const ( OAUTH_GITLAB_FILE = ".gitlab-oauth-token" gitlabBaseURL = "https://gitlab.cri.epita.fr" ) var ( gitlabClientID = "" gitlabSecret = "" gitlaboauth2Config *oauth2.Config gitlabToken func() *oauth2.Token ) func init() { flag.StringVar(&gitlabClientID, "gitlab-clientid", gitlabClientID, "ClientID for GitLab's OIDC") flag.StringVar(&gitlabSecret, "gitlab-secret", gitlabSecret, "Secret for GitLab's OIDC") } func initializeGitLabOIDC(router *gin.Engine, authrouter *gin.RouterGroup, adminrouter *gin.RouterGroup) { adminrouter.GET("/auth/gitlabcri", redirectOAuth_GitLab) router.GET("/callback/gitlabcri/complete", GitLab_OAuth_complete) if gitlabClientID != "" && gitlabSecret != "" { gitlaboauth2Config = &oauth2.Config{ ClientID: gitlabClientID, ClientSecret: gitlabSecret, RedirectURL: oidcRedirectURL + baseURL + "/callback/gitlabcri/complete", // Discovery returns the OAuth2 endpoints. Endpoint: oauth2.Endpoint{ AuthURL: gitlabBaseURL + "/oauth/authorize", TokenURL: gitlabBaseURL + "/oauth/token", }, // "openid" is a required scope for OpenID Connect flows. Scopes: []string{"api", "read_repository", "email"}, } authrouter.GET("/api/gitlabcri/repositories", GitLab_GetMyRepositories) usersRoutes := authrouter.Group("/api/users/:uid") usersRoutes.Use(userHandler) usersRoutes.Use(sameUserMiddleware) usersRoutes.GET("/gitlabcri/repositories", GitLab_GetMyRepositories) } if _, err := os.Stat(OAUTH_GITLAB_FILE); err == nil { tk, err := loadOAuth2Token(OAUTH_GITLAB_FILE) if err != nil { log.Println("Unable to load OAuth2 Token:", err.Error()) } prepareOAuth2Token(tk) } } func loadOAuth2Token(file string) (*oauth2.Token, error) { fd, err := os.Open(file) if err != nil { return nil, err } defer fd.Close() var tok oauth2.Token err = json.NewDecoder(fd).Decode(&tok) return &tok, err } func prepareOAuth2Token(tk *oauth2.Token) { tsource := oauth2.ReuseTokenSource(tk, gitlaboauth2Config.TokenSource(context.Background(), tk)) gitlabToken = func() *oauth2.Token { t, err := tsource.Token() if err != nil { log.Println("Unable to regenerate GitLab token:", err) } return t } } func saveOAuth2Token(file string, tok *oauth2.Token) error { fd, err := os.Create(file) if err != nil { return err } defer fd.Close() return json.NewEncoder(fd).Encode(tok) } func redirectOAuth_GitLab(c *gin.Context) { session := c.MustGet("Session").(*Session) // Save next parameter if len(c.Request.URL.Query().Get("next")) > 0 { session.SetKey("gitlab-oidc-source", c.Request.URL.Query().Get("next")) } c.Redirect(http.StatusFound, gitlaboauth2Config.AuthCodeURL(hex.EncodeToString(session.Id))) } func GitLab_OAuth_complete(c *gin.Context) { idsession, err := hex.DecodeString(c.Request.URL.Query().Get("state")) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) return } session, err := getSession(idsession) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"errmsg": err.Error()}) return } oauth2Token, err := gitlaboauth2Config.Exchange(c.Request.Context(), c.Request.URL.Query().Get("code")) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Failed to exchange token: " + err.Error()}) return } err = saveOAuth2Token(OAUTH_GITLAB_FILE, oauth2Token) if err != nil { c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to save OAuth2 token: " + err.Error()}) return } prepareOAuth2Token(oauth2Token) log.Println("New GitLab OAuth2 session opened") if source, ok := session.GetKey("gitlab-oidc-source"); ok { session.DeleteKey("gitlab-oidc-source") c.Redirect(http.StatusFound, baseURL+source.(string)) } else { c.Redirect(http.StatusFound, baseURL+"/works") } session.Update() } type GitLabRepositoryNamespace struct { ID int Name string Path string Kind string } type GitLabUser struct { ID int Username string Name string State string } type GitLabUserKey struct { ID int Key string } type GitLabRepository struct { ID int Description string Name string Path string PathWithNamespace string `json:"path_with_namespace"` DefaultBranch string `json:"default_branch"` URL string `json:"url,omitempty"` SshUrlToRepo string `json:"ssh_url_to_repo"` HttpUrlToRepo string `json:"http_url_to_repo"` AvatarURL string `json:"avatar_url"` LastActivityAt time.Time `json:"last_activity_at,omitempty"` Namespace GitLabRepositoryNamespace Visibility string `json:"visibility,omitempty"` Owner *GitLabUser `json:"owner,omitempty"` ForkedFromProject *GitLabRepository `json:"forked_from_project,omitempty"` } func GitLab_GetMyRepositories(c *gin.Context) { var u *User if user, ok := c.Get("user"); ok { u = user.(*User) } else { u = c.MustGet("LoggedUser").(*User) } repos, err := GitLab_getUsersRepositories(c.Request.Context(), u) if err != nil { log.Println("Unable to perform the GitLab request: ", err.Error()) c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "An error occurs when performing the GitLab request."}) return } var repositories []*GitLabRepository for _, r := range repos { if r.Owner.Username == u.Login { repositories = append(repositories, r) } } c.JSON(http.StatusOK, repos) } /*func GitLab_getRepositories(c context.Context) ([]*GitLabRepository, error) { client := oauth2.NewClient(c, gitlaboauth2Config.TokenSource(c, gitlabToken)) req, err := http.NewRequest("GET", gitlabBaseURL+"/api/v4/projects?per_page=100", nil) if err != nil { return nil, err } resp, err := client.Do(req) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("Bad status code from the API") } var repositories []*GitLabRepository err = json.NewDecoder(resp.Body).Decode(&repositories) return repositories, err }*/ func GitLab_getUsersRepositories(c context.Context, u *User) ([]*GitLabRepository, error) { if gitlaboauth2Config == nil || gitlabToken == nil { return nil, fmt.Errorf("the connection with GitLab is not setup yet") } client := gitlaboauth2Config.Client(c, gitlabToken()) req, err := http.NewRequest("GET", gitlabBaseURL+fmt.Sprintf("/api/v4/users/%s/projects?per_page=100", u.Login), nil) if err != nil { return nil, err } resp, err := client.Do(req) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("Bad status code from the API") } var repositories []*GitLabRepository err = json.NewDecoder(resp.Body).Decode(&repositories) return repositories, err } func GitLab_getUserId(c context.Context, u *User) (int, error) { client := gitlaboauth2Config.Client(c, gitlabToken()) val := url.Values{} val.Set("username", u.Login) req, err := http.NewRequest("GET", gitlabBaseURL+fmt.Sprintf("/api/v4/users?%s", val.Encode()), nil) if err != nil { return 0, err } resp, err := client.Do(req) if err != nil { return 0, err } if resp.StatusCode != http.StatusOK { return 0, fmt.Errorf("Bad status code from the API") } var users []*GitLabUser err = json.NewDecoder(resp.Body).Decode(&users) if len(users) == 0 { return 0, fmt.Errorf("Login not found in GitLab") } return users[0].ID, nil } func GitLab_getUserPGPKeys(c context.Context, u *User) ([]byte, error) { userid, err := GitLab_getUserId(c, u) if err != nil { return nil, err } client := gitlaboauth2Config.Client(c, gitlabToken()) req, err := http.NewRequest("GET", gitlabBaseURL+fmt.Sprintf("/api/v4/users/%d/gpg_keys", userid), nil) if err != nil { return nil, err } resp, err := client.Do(req) if err != nil { return nil, err } if resp.StatusCode != http.StatusOK { rep, _ := ioutil.ReadAll(resp.Body) log.Printf("%d %s", resp.StatusCode, rep) return nil, fmt.Errorf("Bad status code from the API") } var keys []*GitLabUserKey err = json.NewDecoder(resp.Body).Decode(&keys) var b bytes.Buffer for _, k := range keys { b.Write([]byte(k.Key)) b.Write([]byte{'\n'}) } return b.Bytes(), nil }