Skip to content
This repository was archived by the owner on Jan 31, 2026. It is now read-only.

Commit 1aac837

Browse files
committed
feat: add gitcode thirdparty
feat: add gitcode thirdparty feat: add gitcode thirdparty feat: add gitcode thirdparty
1 parent e4ae5ef commit 1aac837

2 files changed

Lines changed: 392 additions & 0 deletions

File tree

providers/gitcode.go

Lines changed: 390 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,390 @@
1+
package providers
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"net/url"
8+
"path"
9+
"strconv"
10+
"time"
11+
12+
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/apis/sessions"
13+
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
14+
"github.com/oauth2-proxy/oauth2-proxy/v7/pkg/requests"
15+
)
16+
17+
// GitcodeProvider represents an Gitcode based Identity Provider
18+
type GitcodeProvider struct {
19+
*ProviderData
20+
Org string
21+
Repo string
22+
Token string
23+
Users []string
24+
}
25+
26+
type userGitcode struct {
27+
ID string `json:"id"`
28+
Login string `json:"login"`
29+
Email string `json:"email"`
30+
}
31+
32+
var _ Provider = (*GitcodeProvider)(nil)
33+
34+
const (
35+
gitcodeProviderName = "Gitcode"
36+
gitcodeDefaultScope = "all_user"
37+
)
38+
39+
var (
40+
// Default Login URL for Gitcode
41+
// pre-parsed URL of https://gitcode.com/oauth/authorize.
42+
gitcodeDefaultLoginURL = &url.URL{
43+
Scheme: "https",
44+
Host: "gitcode.com",
45+
Path: "/oauth/authorize",
46+
}
47+
// Default Redeem URL for GitHub.
48+
// Pre-parsed URL of https://gitcode.com/oauth/token.
49+
gitcodeDefaultRedeemURL = &url.URL{
50+
Scheme: "https",
51+
Host: "gitcode.com",
52+
Path: "/oauth/token",
53+
}
54+
55+
// Default Validation URL for GitHub.
56+
// ValidationURL is the API Base URL.
57+
// Other API requests are based off of this (eg to fetch users/groups).
58+
// Pre-parsed URL of https://gitcode.com/api/v5.
59+
gitcodeDefaultValidateURL = &url.URL{
60+
Scheme: "https",
61+
Host: "api.gitcode.com",
62+
Path: "/api/v5",
63+
}
64+
)
65+
66+
// NewGitcodeProvider initiates a new GitcodeProvider
67+
func NewGitcodeProvider(p *ProviderData) *GitcodeProvider {
68+
p.setProviderDefaults(providerDefaults{
69+
name: gitcodeProviderName,
70+
loginURL: gitcodeDefaultLoginURL,
71+
redeemURL: gitcodeDefaultRedeemURL,
72+
profileURL: nil,
73+
validateURL: gitcodeDefaultValidateURL,
74+
scope: gitcodeDefaultScope,
75+
})
76+
return &GitcodeProvider{ProviderData: p}
77+
}
78+
79+
// GetEmailAddress returns the Account email address
80+
// Deprecated: Migrate to EnrichSession
81+
func (p *GitcodeProvider) GetEmailAddress(_ context.Context, _ *sessions.SessionState) (string, error) {
82+
return "", nil
83+
}
84+
85+
// Redeem provides a default implementation of the OAuth2 token redemption process
86+
func (p *GitcodeProvider) Redeem(ctx context.Context, redirectURL, code string) (*sessions.SessionState, error) {
87+
if code == "" {
88+
return nil, ErrMissingCode
89+
}
90+
clientSecret, err := p.GetClientSecret()
91+
if err != nil {
92+
return nil, err
93+
}
94+
95+
params := url.Values{}
96+
params.Add("redirect_uri", redirectURL)
97+
params.Add("client_id", p.ClientID)
98+
params.Add("client_secret", clientSecret)
99+
params.Add("code", code)
100+
params.Add("grant_type", "authorization_code")
101+
if p.ProtectedResource != nil && p.ProtectedResource.String() != "" {
102+
params.Add("resource", p.ProtectedResource.String())
103+
}
104+
105+
result := requests.New(p.RedeemURL.String()).
106+
WithContext(ctx).
107+
WithMethod("POST").
108+
WithBody(bytes.NewBufferString(params.Encode())).
109+
SetHeader("Content-Type", "application/x-www-form-urlencoded").
110+
Do()
111+
if result.Error() != nil {
112+
return nil, result.Error()
113+
}
114+
fmt.Println(string(result.Body()))
115+
// blindly try json and x-www-form-urlencoded
116+
var jsonResponse struct {
117+
AccessToken string `json:"access_token"`
118+
ExpiresIn int64 `json:"expires_in"`
119+
RefreshToken string `json:"refresh_token"`
120+
Scope string `json:"scope"`
121+
CreatedAt string `json:"created_at"`
122+
}
123+
err = result.UnmarshalInto(&jsonResponse)
124+
if err != nil {
125+
return nil, err
126+
}
127+
timestampNow := time.Now().Unix()
128+
// 使用 RFC3339Nano 解析(兼容毫秒)
129+
t, err_ := time.Parse(time.RFC3339Nano, jsonResponse.CreatedAt)
130+
if err_ != nil {
131+
fmt.Println("parse created_at error:" + jsonResponse.CreatedAt)
132+
} else {
133+
timestampNow = t.Unix()
134+
}
135+
expiresTime := timestampNow + jsonResponse.ExpiresIn
136+
137+
expiresOn := time.Unix(expiresTime, 0)
138+
ca := time.Unix(timestampNow, 0)
139+
p.Scope = jsonResponse.Scope
140+
return &sessions.SessionState{
141+
AccessToken: jsonResponse.AccessToken,
142+
RefreshToken: jsonResponse.RefreshToken,
143+
ExpiresOn: &expiresOn,
144+
CreatedAt: &ca,
145+
}, nil
146+
147+
}
148+
149+
// EnrichSession updates the User & Email after the initial Redeem
150+
func (p *GitcodeProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
151+
p.Scope = gitcodeDefaultScope
152+
return p.getUser(ctx, s)
153+
}
154+
155+
func (p *GitcodeProvider) getUser(ctx context.Context, s *sessions.SessionState) error {
156+
user, err := p.userInfo(ctx, s.AccessToken)
157+
if err != nil {
158+
return err
159+
}
160+
// determine whether user is belong to org add code on this place
161+
// Now that we have the username we can check collaborator status
162+
if !p.isVerifiedUser(user.Login) && p.Org == "" && p.Repo != "" && p.Token != "" {
163+
if ok, err := p.isCollaborator(ctx, user.Login, p.Token); err != nil || !ok {
164+
return err
165+
}
166+
}
167+
168+
s.User = user.Login
169+
s.Email = user.Email
170+
return nil
171+
}
172+
173+
// RefreshSessionIfNeeded checks if the session has expired and uses the
174+
// RefreshToken to fetch a new ID token if required
175+
func (p *GitcodeProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions.SessionState) (bool, error) {
176+
if s == nil || (s.ExpiresOn != nil && s.ExpiresOn.After(time.Now())) || s.RefreshToken == "" {
177+
return false, nil
178+
}
179+
origExpiration := s.ExpiresOn
180+
181+
err := p.redeemRefreshToken(ctx, s)
182+
if err != nil {
183+
return false, fmt.Errorf("unable to redeem refresh token: %v", err)
184+
}
185+
186+
fmt.Printf("refreshed id token %s (expired on %s)\n", s, origExpiration)
187+
return true, nil
188+
}
189+
190+
func (p *GitcodeProvider) redeemRefreshToken(ctx context.Context, s *sessions.SessionState) error {
191+
params := url.Values{}
192+
params.Add("refresh_token", s.RefreshToken)
193+
params.Add("grant_type", "refresh_token")
194+
result := requests.New(p.RedeemURL.String()).
195+
WithContext(ctx).
196+
WithMethod("POST").
197+
WithBody(bytes.NewBufferString(params.Encode())).
198+
SetHeader("Content-Type", "application/x-www-form-urlencoded").
199+
Do()
200+
if result.Error() != nil {
201+
return result.Error()
202+
}
203+
fmt.Println(string(result.Body()))
204+
// blindly try json and x-www-form-urlencoded
205+
var jsonResponse struct {
206+
AccessToken string `json:"access_token"`
207+
ExpiresIn int64 `json:"expires_in"`
208+
RefreshToken string `json:"refresh_token"`
209+
Scope string `json:"scope"`
210+
CreateAt int64 `json:"create_at"`
211+
}
212+
err := result.UnmarshalInto(&jsonResponse)
213+
if err != nil {
214+
return err
215+
}
216+
expiresTime := jsonResponse.CreateAt + jsonResponse.ExpiresIn
217+
expiresOn := time.Unix(expiresTime, 0)
218+
ca := time.Unix(jsonResponse.CreateAt, 0)
219+
p.Scope = jsonResponse.Scope
220+
s.RefreshToken = jsonResponse.RefreshToken
221+
s.ExpiresOn = &expiresOn
222+
s.CreatedAt = &ca
223+
return nil
224+
}
225+
226+
func (p *GitcodeProvider) hasUser(ctx context.Context, accessToken string) (bool, error) {
227+
user, err := p.userInfo(ctx, accessToken)
228+
if err != nil {
229+
return false, err
230+
}
231+
if p.isVerifiedUser(user.Login) {
232+
return true, nil
233+
}
234+
return false, nil
235+
}
236+
237+
func (p *GitcodeProvider) userInfo(ctx context.Context, accessToken string) (userGitcode, error) {
238+
var user userGitcode
239+
endpoint := &url.URL{
240+
Scheme: p.ValidateURL.Scheme,
241+
Host: p.ValidateURL.Host,
242+
Path: path.Join(p.ValidateURL.Path, "/user"),
243+
}
244+
err := requests.New(endpoint.String()).
245+
WithContext(ctx).
246+
SetHeader("Authorization", "Bearer "+accessToken).
247+
SetHeader("Content-Type", "application/json;charset=UTF-8").
248+
Do().
249+
UnmarshalInto(&user)
250+
return user, err
251+
}
252+
253+
func (p *GitcodeProvider) isVerifiedUser(username string) bool {
254+
for _, u := range p.Users {
255+
if username == u {
256+
return true
257+
}
258+
}
259+
return false
260+
}
261+
262+
func (p *GitcodeProvider) isCollaborator(ctx context.Context, login string, accessToken string) (bool, error) {
263+
params := url.Values{}
264+
params.Add("access_token", accessToken)
265+
endpoint := &url.URL{
266+
Scheme: p.ValidateURL.Scheme,
267+
Host: p.ValidateURL.Host,
268+
Path: path.Join(p.ValidateURL.Path, "/repos/", p.Repo, "collaborators", login),
269+
RawQuery: params.Encode(),
270+
}
271+
result := requests.New(endpoint.String()).
272+
WithContext(ctx).
273+
SetHeader("Content-Type", "application/json;charset=UTF-8").
274+
Do()
275+
if result.Error() != nil {
276+
return false, result.Error()
277+
}
278+
279+
if result.StatusCode() != 204 {
280+
return false, fmt.Errorf("got %d from %q %s",
281+
result.StatusCode(), endpoint.String(), result.Body())
282+
}
283+
284+
logger.Printf("got %d from %q %s", result.StatusCode(), endpoint.String(), result.Body())
285+
286+
return true, nil
287+
288+
}
289+
290+
func (p *GitcodeProvider) hasOrg(ctx context.Context, accessToken string) (bool, error) {
291+
type orgsPage []struct {
292+
Login string `json:"login"`
293+
}
294+
295+
var orgs []struct {
296+
Login string `json:"login"`
297+
}
298+
299+
pn := 1
300+
ppg := 20
301+
params := url.Values{}
302+
params.Add("access_token", accessToken)
303+
for {
304+
params.Set("page", strconv.Itoa(pn))
305+
params.Set("per_page", strconv.Itoa(ppg))
306+
endpoint := &url.URL{
307+
Scheme: p.ValidateURL.Scheme,
308+
Host: p.ValidateURL.Host,
309+
Path: path.Join(p.ValidateURL.Path, "/user/orgs"),
310+
RawQuery: params.Encode(),
311+
}
312+
var op orgsPage
313+
err := requests.New(endpoint.String()).
314+
WithContext(ctx).
315+
SetHeader("Content-Type", "application/json;charset=UTF-8").
316+
Do().UnmarshalInto(&op)
317+
if err != nil {
318+
return false, err
319+
}
320+
if len(op) == 0 {
321+
break
322+
}
323+
orgs = append(orgs, op...)
324+
if len(op) < ppg {
325+
break
326+
}
327+
pn++
328+
}
329+
presentOrgs := make([]string, 0, len(orgs))
330+
for _, org := range orgs {
331+
if p.Org == org.Login {
332+
logger.Printf("found gitcode organization: %q", org.Login)
333+
return true, nil
334+
}
335+
presentOrgs = append(presentOrgs, org.Login)
336+
}
337+
338+
logger.Printf("missing organization:%q in %v", p.Org, presentOrgs)
339+
return false, nil
340+
}
341+
342+
func (p *GitcodeProvider) hasRepo(ctx context.Context, accessToken string) (bool, error) {
343+
type permissions struct {
344+
Pull bool `json:"pull"`
345+
Push bool `json:"push"`
346+
}
347+
348+
type repository struct {
349+
Permissions permissions `json:"permissions"`
350+
Private bool `json:"private"`
351+
}
352+
endpoint := &url.URL{
353+
Scheme: p.ValidateURL.Scheme,
354+
Host: p.ValidateURL.Host,
355+
Path: path.Join(p.ValidateURL.Path, "repos", p.Repo),
356+
}
357+
358+
var repo repository
359+
err := requests.New(endpoint.String()).
360+
WithContext(ctx).
361+
WithHeaders(makeGitHubHeader(accessToken)).
362+
Do().
363+
UnmarshalInto(&repo)
364+
if err != nil {
365+
return false, err
366+
}
367+
368+
// Every user can implicitly pull from a public repo, so only grant access
369+
// if they have push access or the repo is private and they can pull
370+
return repo.Permissions.Push || (repo.Private && repo.Permissions.Pull), nil
371+
}
372+
373+
// SetRepo configures the target repository and optional token to use
374+
func (p *GitcodeProvider) SetRepo(repo, token string) {
375+
p.Repo = repo
376+
p.Token = token
377+
}
378+
379+
// SetUsers configures allowed usernames
380+
func (p *GitcodeProvider) SetUsers(users []string) {
381+
p.Users = users
382+
}
383+
384+
// SetOrg adds Gitcode org reading parameters to the OAuth2 scope
385+
func (p *GitcodeProvider) SetOrg(org string) {
386+
p.Org = org
387+
if org != "" {
388+
p.Scope += " groups"
389+
}
390+
}

providers/providers.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ func New(provider string, p *ProviderData) Provider {
3131
return NewGitHubProvider(p)
3232
case "gitee":
3333
return NewGiteeProvider(p)
34+
case "gitcode":
35+
return NewGitcodeProvider(p)
3436
case "keycloak":
3537
return NewKeycloakProvider(p)
3638
case "azure":

0 commit comments

Comments
 (0)