From 545611ae72ae13428f699bd26b521331d093a4b8 Mon Sep 17 00:00:00 2001 From: Alex Gartner Date: Sun, 19 May 2019 15:46:38 -0700 Subject: [PATCH] gitlab: use /oauth/userinfo and respect GITLAB_URL --- oauth2/gitlab.go | 155 ++++++++++++++++------------------- oauth2/gitlab_test.go | 184 ++++++++---------------------------------- 2 files changed, 102 insertions(+), 237 deletions(-) diff --git a/oauth2/gitlab.go b/oauth2/gitlab.go index a4023300..ecd7f2e5 100644 --- a/oauth2/gitlab.go +++ b/oauth2/gitlab.go @@ -5,102 +5,87 @@ import ( "fmt" "io/ioutil" "net/http" + "os" "strings" "github.com/tarent/loginsrv/model" ) -var gitlabAPI = "https://gitlab.com/api/v4" +const defaultGitlabURL = "https://gitlab.com" func init() { - RegisterProvider(providerGitlab) + gitlabURL, ok := os.LookupEnv("GITLAB_URL") + if !ok { + gitlabURL = defaultGitlabURL + } + provider := MakeGitlabProvider(gitlabURL) + RegisterProvider(provider) } -// GitlabUser is used for parsing the gitlab response -type GitlabUser struct { - Username string `json:"username,omitempty"` - AvatarURL string `json:"avatar_url,omitempty"` - Name string `json:"name,omitempty"` - Email string `json:"email,omitempty"` +// GitlabUserInfo is used for parsing the gitlab response +type GitlabUserInfo struct { + model.UserInfo + Sub string `json:"nickname"` } -type GitlabGroup struct { - FullPath string `json:"full_path,omitempty"` +func (i GitlabUserInfo) toUserInfo() model.UserInfo { + res := model.UserInfo{ + Sub: i.Sub, + Picture: i.Picture, + Name: i.Name, + Email: i.Email, + Origin: "gitlab", + Expiry: i.Expiry, + Refreshes: i.Refreshes, + Groups: i.Groups, + } + + email := i.Email + emailComponents := strings.Split(email, "@") + if len(emailComponents) == 2 { + res.Domain = emailComponents[1] + } + return res } -var providerGitlab = Provider{ - Name: "gitlab", - AuthURL: "https://gitlab.com/oauth/authorize", - TokenURL: "https://gitlab.com/oauth/token", - GetUserInfo: func(token TokenInfo) (model.UserInfo, string, error) { - gu := GitlabUser{} - url := fmt.Sprintf("%v/user?access_token=%v", gitlabAPI, token.AccessToken) - - var respUser *http.Response - respUser, err := http.Get(url) - if err != nil { - return model.UserInfo{}, "", err - } - defer respUser.Body.Close() - - if !strings.Contains(respUser.Header.Get("Content-Type"), "application/json") { - return model.UserInfo{}, "", fmt.Errorf("wrong content-type on gitlab get user info: %v", respUser.Header.Get("Content-Type")) - } - - if respUser.StatusCode != 200 { - return model.UserInfo{}, "", fmt.Errorf("got http status %v on gitlab get user info", respUser.StatusCode) - } - - b, err := ioutil.ReadAll(respUser.Body) - if err != nil { - return model.UserInfo{}, "", fmt.Errorf("error reading gitlab get user info: %v", err) - } - - err = json.Unmarshal(b, &gu) - if err != nil { - return model.UserInfo{}, "", fmt.Errorf("error parsing gitlab get user info: %v", err) - } - - gg := []*GitlabGroup{} - url = fmt.Sprintf("%v/groups?access_token=%v", gitlabAPI, token.AccessToken) - - var respGroup *http.Response - respGroup, err = http.Get(url) - if err != nil { - return model.UserInfo{}, "", err - } - defer respGroup.Body.Close() - - if !strings.Contains(respGroup.Header.Get("Content-Type"), "application/json") { - return model.UserInfo{}, "", fmt.Errorf("wrong content-type on gitlab get groups info: %v", respGroup.Header.Get("Content-Type")) - } - - if respGroup.StatusCode != 200 { - return model.UserInfo{}, "", fmt.Errorf("got http status %v on gitlab get groups info", respGroup.StatusCode) - } - - g, err := ioutil.ReadAll(respGroup.Body) - if err != nil { - return model.UserInfo{}, "", fmt.Errorf("error reading gitlab get groups info: %v", err) - } - - err = json.Unmarshal(g, &gg) - if err != nil { - return model.UserInfo{}, "", fmt.Errorf("error parsing gitlab get groups info: %v", err) - } - - groups := make([]string, len(gg)) - for i := 0; i < len(gg); i++ { - groups[i] = gg[i].FullPath - } - - return model.UserInfo{ - Sub: gu.Username, - Picture: gu.AvatarURL, - Name: gu.Name, - Email: gu.Email, - Groups: groups, - Origin: "gitlab", - }, `{"user":` + string(b) + `,"groups":` + string(g) + `}`, nil - }, +// MakeGitlabProvider make's a gitlab provider with the given url +func MakeGitlabProvider(gitlabURL string) Provider { + return Provider{ + Name: "gitlab", + AuthURL: gitlabURL + "/oauth/authorize", + TokenURL: gitlabURL + "/oauth/token", + DefaultScopes: "email openid", + GetUserInfo: func(token TokenInfo) (model.UserInfo, string, error) { + url := fmt.Sprintf("%s/oauth/userinfo?access_token=%v", gitlabURL, token.AccessToken) + + var info GitlabUserInfo + + var respUser *http.Response + respUser, err := http.Get(url) + if err != nil { + return model.UserInfo{}, "", err + } + defer respUser.Body.Close() + + if !strings.Contains(respUser.Header.Get("Content-Type"), "application/json") { + return model.UserInfo{}, "", fmt.Errorf("wrong content-type on gitlab get user info: %v", respUser.Header.Get("Content-Type")) + } + + if respUser.StatusCode != 200 { + return model.UserInfo{}, "", fmt.Errorf("got http status %v on gitlab get user info", respUser.StatusCode) + } + + b, err := ioutil.ReadAll(respUser.Body) + if err != nil { + return model.UserInfo{}, "", fmt.Errorf("error reading gitlab get user info: %v", err) + } + + err = json.Unmarshal(b, &info) + if err != nil { + return model.UserInfo{}, "", fmt.Errorf("error parsing gitlab get user info: %v", err) + } + + return info.toUserInfo(), "", nil + }, + } } diff --git a/oauth2/gitlab_test.go b/oauth2/gitlab_test.go index 78ecde30..ef86a51a 100644 --- a/oauth2/gitlab_test.go +++ b/oauth2/gitlab_test.go @@ -11,103 +11,42 @@ import ( ) var gitlabTestUserResponse = `{ - "id": 1, - "username": "john_smith", - "email": "john@example.com", + "sub": "1234567", + "sub_legacy": "e7d33ae82f57ec69415af7dadb01f7b047ad62fd3a7d7957f20d6ceb7643331a", "name": "John Smith", - "state": "active", - "avatar_url": "http://localhost:3000/uploads/user/avatar/1/index.jpg", - "web_url": "http://localhost:3000/john_smith", - "created_at": "2012-05-23T08:00:58Z", - "bio": null, - "location": null, - "public_email": "john@example.com", - "skype": "", - "linkedin": "", - "twitter": "", - "website_url": "", - "organization": "", - "last_sign_in_at": "2012-06-01T11:41:01Z", - "confirmed_at": "2012-05-23T09:05:22Z", - "theme_id": 1, - "last_activity_on": "2012-05-23", - "color_scheme_id": 2, - "projects_limit": 100, - "current_sign_in_at": "2012-06-02T06:36:55Z", - "identities": [ - {"provider": "github", "extern_uid": "2435223452345"}, - {"provider": "bitbucket", "extern_uid": "john_smith"}, - {"provider": "google_oauth2", "extern_uid": "8776128412476123468721346"} - ], - "can_create_group": true, - "can_create_project": true, - "two_factor_enabled": true, - "external": false, - "private_profile": false + "nickname": "john_smith", + "email": "john@example.com", + "email_verified": true, + "profile": "https://gitlab.com/jsmith", + "picture": "https://secure.gravatar.com/avatar/b92a7c822a31fa55c65186f9be24841e?s=80&d=identicon", + "groups": [ + "example", + "example/subgroup" + ] }` -var gitlabTestGroupsResponse = `[ - { - "id": 1, - "web_url": "https://gitlab.com/groups/example", - "name": "example", - "path": "example", - "description": "", - "visibility": "private", - "lfs_enabled": true, - "avatar_url": null, - "request_access_enabled": true, - "full_name": "example", - "full_path": "example", - "parent_id": null, - "ldap_cn": null, - "ldap_access": null - }, - { - "id": 2, - "web_url": "https://gitlab.com/groups/example/subgroup", - "name": "subgroup", - "path": "subgroup", - "description": "", - "visibility": "private", - "lfs_enabled": true, - "avatar_url": null, - "request_access_enabled": true, - "full_name": "example / subgroup", - "full_path": "example/subgroup", - "parent_id": null, - "ldap_cn": null, - "ldap_access": null - } -]` - func Test_Gitlab_getUserInfo(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/user" { + if r.URL.Path == "/oauth/userinfo" { Equal(t, "secret", r.FormValue("access_token")) w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Write([]byte(gitlabTestUserResponse)) - } else if r.URL.Path == "/groups" { - Equal(t, "secret", r.FormValue("access_token")) - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Write([]byte(gitlabTestGroupsResponse)) } })) defer server.Close() - gitlabAPI = server.URL + providerGitlab := MakeGitlabProvider(server.URL) - u, rawJSON, err := providerGitlab.GetUserInfo(TokenInfo{AccessToken: "secret"}) + u, _, err := providerGitlab.GetUserInfo(TokenInfo{AccessToken: "secret"}) NoError(t, err) Equal(t, "john_smith", u.Sub) Equal(t, "john@example.com", u.Email) Equal(t, "John Smith", u.Name) Equal(t, []string{"example", "example/subgroup"}, u.Groups) - Equal(t, `{"user":`+gitlabTestUserResponse+`,"groups":`+gitlabTestGroupsResponse+`}`, rawJSON) } func Test_Gitlab_getUserInfo_NoServer(t *testing.T) { - gitlabAPI = "http://localhost" + providerGitlab := MakeGitlabProvider("http://localhost:8290") u, rawJSON, err := providerGitlab.GetUserInfo(TokenInfo{AccessToken: "secret"}) Equal(t, model.UserInfo{}, u) @@ -118,19 +57,15 @@ func Test_Gitlab_getUserInfo_NoServer(t *testing.T) { func Test_Gitlab_getUserInfo_UserContentTypeNegative(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/user" { + if r.URL.Path == "/oauth/userinfo" { Equal(t, "secret", r.FormValue("access_token")) w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Write([]byte(gitlabTestUserResponse)) - } else if r.URL.Path == "/groups" { - Equal(t, "secret", r.FormValue("access_token")) - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Write([]byte(gitlabTestGroupsResponse)) } })) defer server.Close() - gitlabAPI = server.URL + providerGitlab := MakeGitlabProvider(server.URL) u, rawJSON, err := providerGitlab.GetUserInfo(TokenInfo{AccessToken: "secret"}) Equal(t, model.UserInfo{}, u) @@ -139,45 +74,18 @@ func Test_Gitlab_getUserInfo_UserContentTypeNegative(t *testing.T) { Regexp(t, regexp.MustCompile(`^wrong content-type on gitlab get user info`), err.Error()) } -func Test_Gitlab_getUserInfo_GroupsContentTypeNegative(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/user" { - Equal(t, "secret", r.FormValue("access_token")) - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Write([]byte(gitlabTestUserResponse)) - } else if r.URL.Path == "/groups" { - Equal(t, "secret", r.FormValue("access_token")) - w.Header().Set("Content-Type", "text/plain; charset=utf-8") - w.Write([]byte(gitlabTestGroupsResponse)) - } - })) - defer server.Close() - - gitlabAPI = server.URL - - u, rawJSON, err := providerGitlab.GetUserInfo(TokenInfo{AccessToken: "secret"}) - Equal(t, model.UserInfo{}, u) - Empty(t, rawJSON) - Error(t, err) - Regexp(t, regexp.MustCompile(`^wrong content-type on gitlab get groups info`), err.Error()) -} - func Test_Gitlab_getUserInfo_UserStatusCodeNegative(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/user" { + if r.URL.Path == "/oauth/userinfo" { Equal(t, "secret", r.FormValue("access_token")) w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(gitlabTestUserResponse)) - } else if r.URL.Path == "/groups" { - Equal(t, "secret", r.FormValue("access_token")) - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Write([]byte(gitlabTestGroupsResponse)) } })) defer server.Close() - gitlabAPI = server.URL + providerGitlab := MakeGitlabProvider(server.URL) u, rawJSON, err := providerGitlab.GetUserInfo(TokenInfo{AccessToken: "secret"}) Equal(t, model.UserInfo{}, u) @@ -186,72 +94,44 @@ func Test_Gitlab_getUserInfo_UserStatusCodeNegative(t *testing.T) { Regexp(t, regexp.MustCompile(`^got http status [0-9]{3} on gitlab get user info`), err.Error()) } -func Test_Gitlab_getUserInfo_GroupsStatusCodeNegative(t *testing.T) { +func Test_Gitlab_getUserInfo_UserReadNegative(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/user" { - Equal(t, "secret", r.FormValue("access_token")) - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Write([]byte(gitlabTestUserResponse)) - } else if r.URL.Path == "/groups" { - Equal(t, "secret", r.FormValue("access_token")) + if r.URL.Path == "/oauth/userinfo" { w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(http.StatusInternalServerError) - w.Write([]byte(gitlabTestGroupsResponse)) + w.Write([]byte("")) + + // hijack the connection to force close + hj, _ := w.(http.Hijacker) + conn, _, _ := hj.Hijack() + conn.Close(); } })) defer server.Close() - gitlabAPI = server.URL + providerGitlab := MakeGitlabProvider(server.URL) u, rawJSON, err := providerGitlab.GetUserInfo(TokenInfo{AccessToken: "secret"}) Equal(t, model.UserInfo{}, u) Empty(t, rawJSON) Error(t, err) - Regexp(t, regexp.MustCompile(`^got http status [0-9]{3} on gitlab get groups info`), err.Error()) + Regexp(t, regexp.MustCompile(`^error reading gitlab get user info`), err.Error()) } func Test_Gitlab_getUserInfo_UserJSONNegative(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/user" { + if r.URL.Path == "/oauth/userinfo" { Equal(t, "secret", r.FormValue("access_token")) w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Write([]byte("[]")) - } else if r.URL.Path == "/groups" { - Equal(t, "secret", r.FormValue("access_token")) - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Write([]byte(gitlabTestGroupsResponse)) } })) defer server.Close() - gitlabAPI = server.URL + providerGitlab := MakeGitlabProvider(server.URL) u, rawJSON, err := providerGitlab.GetUserInfo(TokenInfo{AccessToken: "secret"}) Equal(t, model.UserInfo{}, u) Empty(t, rawJSON) Error(t, err) Regexp(t, regexp.MustCompile(`^error parsing gitlab get user info`), err.Error()) -} - -func Test_Gitlab_getUserInfo_GroupsJSONNegative(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/user" { - Equal(t, "secret", r.FormValue("access_token")) - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Write([]byte(gitlabTestUserResponse)) - } else if r.URL.Path == "/groups" { - Equal(t, "secret", r.FormValue("access_token")) - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.Write([]byte("{}")) - } - })) - defer server.Close() - - gitlabAPI = server.URL - - u, rawJSON, err := providerGitlab.GetUserInfo(TokenInfo{AccessToken: "secret"}) - Equal(t, model.UserInfo{}, u) - Empty(t, rawJSON) - Error(t, err) - Regexp(t, regexp.MustCompile(`^error parsing gitlab get groups info`), err.Error()) -} +} \ No newline at end of file