diff --git a/idp/gitee.go b/idp/gitee.go new file mode 100644 index 00000000..a150a6fe --- /dev/null +++ b/idp/gitee.go @@ -0,0 +1,230 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package idp + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "golang.org/x/oauth2" +) + +type GiteeIdProvider struct { + Client *http.Client + Config *oauth2.Config +} + +func NewGiteeIdProvider(clientId string, clientSecret string, redirectUrl string) *GiteeIdProvider { + idp := &GiteeIdProvider{} + + config := idp.getConfig(clientId, clientSecret, redirectUrl) + idp.Config = config + + return idp +} + +func (idp *GiteeIdProvider) SetHttpClient(client *http.Client) { + idp.Client = client +} + +// getConfig return a point of Config, which describes a typical 3-legged OAuth2 flow +func (idp *GiteeIdProvider) getConfig(clientId string, clientSecret string, redirectUrl string) *oauth2.Config { + var endpoint = oauth2.Endpoint{ + TokenURL: "https://gitee.com/oauth/token", + } + + var config = &oauth2.Config{ + Scopes: []string{"user_info emails"}, + + Endpoint: endpoint, + ClientID: clientId, + ClientSecret: clientSecret, + RedirectURL: redirectUrl, + } + + return config +} + +type GiteeAccessToken struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + RefreshToken string `json:"refresh_token"` + Scope string `json:"scope"` + CreatedAt int `json:"created_at"` +} + +// GetToken use code get access_token (*operation of getting code ought to be done in front) +// The POST Url format of submission is: https://gitee.com/oauth/token?grant_type=authorization_code&code={code}&client_id={client_id}&redirect_uri={redirect_uri}&client_secret={client_secret} +// get more detail via: https://gitee.com/api/v5/oauth_doc#/ +func (idp *GiteeIdProvider) GetToken(code string) (*oauth2.Token, error) { + params := url.Values{} + params.Add("grant_type", "authorization_code") + params.Add("client_id", idp.Config.ClientID) + params.Add("client_secret", idp.Config.ClientSecret) + params.Add("code", code) + params.Add("redirect_uri", idp.Config.RedirectURL) + + accessTokenUrl := fmt.Sprintf("%s?%s", idp.Config.Endpoint.TokenURL, params.Encode()) + bs, _ := json.Marshal(params.Encode()) + req, _ := http.NewRequest("POST", accessTokenUrl, strings.NewReader(string(bs))) + req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Safari/537.36") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, err + } + rbs, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + tokenResp := GiteeAccessToken{} + if err = json.Unmarshal(rbs, &tokenResp); err != nil { + return nil, err + } + + token := &oauth2.Token{ + AccessToken: tokenResp.AccessToken, + TokenType: tokenResp.TokenType, + RefreshToken: tokenResp.RefreshToken, + Expiry: time.Unix(time.Now().Unix()+int64(tokenResp.ExpiresIn), 0), + } + + return token, nil +} + +/* +{ + "id": 999999, + "login": "xxx", + "name": "xxx", + "avatar_url": "https://gitee.com/assets/no_portrait.png", + "url": "https://gitee.com/api/v5/users/xxx", + "html_url": "https://gitee.com/xxx", + "followers_url": "https://gitee.com/api/v5/users/xxx/followers", + "following_url": "https://gitee.com/api/v5/users/xxx/following_url{/other_user}", + "gists_url": "https://gitee.com/api/v5/users/xxx/gists{/gist_id}", + "starred_url": "https://gitee.com/api/v5/users/xxx/starred{/owner}{/repo}", + "subscriptions_url": "https://gitee.com/api/v5/users/xxx/subscriptions", + "organizations_url": "https://gitee.com/api/v5/users/xxx/orgs", + "repos_url": "https://gitee.com/api/v5/users/xxx/repos", + "events_url": "https://gitee.com/api/v5/users/xxx/events{/privacy}", + "received_events_url": "https://gitee.com/api/v5/users/xxx/received_events", + "type": "User", + "blog": null, + "weibo": null, + "bio": "个人博客:https://gitee.com/xxx/xxx/pages", + "public_repos": 2, + "public_gists": 0, + "followers": 0, + "following": 0, + "stared": 0, + "watched": 2, + "created_at": "2019-08-03T23:21:16+08:00", + "updated_at": "2021-06-14T12:47:09+08:00", + "email": null +} +*/ + +type GiteeUserResponse struct { + AvatarUrl string `json:"avatar_url"` + Bio string `json:"bio"` + Blog string `json:"blog"` + CreatedAt string `json:"created_at"` + Email string `json:"email"` + EventsUrl string `json:"events_url"` + Followers int `json:"followers"` + FollowersUrl string `json:"followers_url"` + Following int `json:"following"` + FollowingUrl string `json:"following_url"` + GistsUrl string `json:"gists_url"` + HtmlUrl string `json:"html_url"` + Id int `json:"id"` + Login string `json:"login"` + MemberRole string `json:"member_role"` + Name string `json:"name"` + OrganizationsUrl string `json:"organizations_url"` + PublicGists int `json:"public_gists"` + PublicRepos int `json:"public_repos"` + ReceivedEventsUrl string `json:"received_events_url"` + ReposUrl string `json:"repos_url"` + Stared int `json:"stared"` + StarredUrl string `json:"starred_url"` + SubscriptionsUrl string `json:"subscriptions_url"` + Type string `json:"type"` + UpdatedAt string `json:"updated_at"` + Url string `json:"url"` + Watched int `json:"watched"` + Weibo string `json:"weibo"` +} + +// GetUserInfo Use userid and access_token to get UserInfo +// get more detail via: https://gitee.com/api/v5/swagger#/getV5User +func (idp *GiteeIdProvider) GetUserInfo(token *oauth2.Token) (*UserInfo, error) { + var gtUserInfo GiteeUserResponse + accessToken := token.AccessToken + + u := fmt.Sprintf("https://gitee.com/api/v5/user?access_token=%s", + accessToken) + + userinfoResp, err := idp.GetUrlResp(u) + if err != nil { + return nil, err + } + + if err = json.Unmarshal([]byte(userinfoResp), >UserInfo); err != nil { + return nil, err + } + + userInfo := UserInfo{ + Id: strconv.Itoa(gtUserInfo.Id), + Username: gtUserInfo.Name, + DisplayName: gtUserInfo.Name, + Email: gtUserInfo.Email, + AvatarUrl: gtUserInfo.AvatarUrl, + } + + return &userInfo, nil +} + +func (idp *GiteeIdProvider) GetUrlResp(url string) (string, error) { + resp, err := idp.Client.Get(url) + if err != nil { + return "", err + } + + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + return + } + }(resp.Body) + + buf := new(bytes.Buffer) + _, err = buf.ReadFrom(resp.Body) + if err != nil { + return "", err + } + + return buf.String(), nil +} diff --git a/idp/provider.go b/idp/provider.go index d6e0c854..c9ca16d9 100644 --- a/idp/provider.go +++ b/idp/provider.go @@ -49,6 +49,8 @@ func GetIdProvider(providerType string, clientId string, clientSecret string, re return NewDingTalkIdProvider(clientId, clientSecret, redirectUrl) } else if providerType == "Weibo" { return NewWeiBoIdProvider(clientId, clientSecret, redirectUrl) + } else if providerType == "Gitee" { + return NewGiteeIdProvider(clientId, clientSecret, redirectUrl) } return nil diff --git a/object/user.go b/object/user.go index 62f0efe9..a3e25c96 100644 --- a/object/user.go +++ b/object/user.go @@ -52,6 +52,7 @@ type User struct { Facebook string `xorm:"facebook varchar(100)" json:"facebook"` DingTalk string `xorm:"dingtalk varchar(100)" json:"dingtalk"` Weibo string `xorm:"weibo varchar(100)" json:"weibo"` + Gitee string `xorm:"gitee varchar(100)" json:"gitee"` Properties map[string]string `json:"properties"` } diff --git a/web/src/ProviderEditPage.js b/web/src/ProviderEditPage.js index c3f439b9..bd84aa4c 100644 --- a/web/src/ProviderEditPage.js +++ b/web/src/ProviderEditPage.js @@ -73,6 +73,7 @@ class ProviderEditPage extends React.Component { {id: 'Facebook', name: 'Facebook'}, {id: 'DingTalk', name: 'DingTalk'}, {id: 'Weibo', name: 'Weibo'}, + {id: 'Gitee', name: 'Gitee'} ] ); } else if (provider.category === "Email") { diff --git a/web/src/auth/Provider.js b/web/src/auth/Provider.js index 0d59f539..a846edca 100644 --- a/web/src/auth/Provider.js +++ b/web/src/auth/Provider.js @@ -43,6 +43,10 @@ const WeiboAuthScope = "email"; const WeiboAuthUri = "https://api.weibo.com/oauth2/authorize"; const WeiboAuthLogo = `${StaticBaseUrl}/img/social_weibo.png`; +const GiteeAuthScope = "user_info emails"; +const GiteeAuthUri = "https://gitee.com/oauth/authorize"; +const GiteeAuthLogo = `${StaticBaseUrl}/img/social_gitee.png`; + export function getAuthLogo(provider) { if (provider.type === "Google") { return GoogleAuthLogo; @@ -58,6 +62,8 @@ export function getAuthLogo(provider) { return DingTalkAuthLogo; } else if (provider.type === "Weibo") { return WeiboAuthLogo; + } else if (provider.type === "Gitee") { + return GiteeAuthLogo; } } @@ -82,5 +88,7 @@ export function getAuthUrl(application, provider, method) { return `${DingTalkAuthUri}?appid=${provider.clientId}&redirect_uri=${redirectUri}&scope=snsapi_login&response_type=code&state=${state}`; } else if (provider.type === "Weibo") { return `${WeiboAuthUri}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${WeiboAuthScope}&response_type=code&state=${state}`; + } else if (provider.type === "Gitee") { + return `${GiteeAuthUri}?client_id=${provider.clientId}&redirect_uri=${redirectUri}&scope=${GiteeAuthScope}&response_type=code&state=${state}`; } }