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

Commit 42d8bbb

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

2 files changed

Lines changed: 383 additions & 0 deletions

File tree

providers/gitcode.go

Lines changed: 381 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,381 @@
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 int64 `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 int64 `json:"created_at"`
122+
}
123+
err = result.UnmarshalInto(&jsonResponse)
124+
if err != nil {
125+
return nil, err
126+
}
127+
expiresTime := jsonResponse.CreatedAt + jsonResponse.ExpiresIn
128+
expiresOn := time.Unix(expiresTime, 0)
129+
ca := time.Unix(jsonResponse.CreatedAt, 0)
130+
p.Scope = jsonResponse.Scope
131+
return &sessions.SessionState{
132+
AccessToken: jsonResponse.AccessToken,
133+
RefreshToken: jsonResponse.RefreshToken,
134+
ExpiresOn: &expiresOn,
135+
CreatedAt: &ca,
136+
}, nil
137+
138+
}
139+
140+
// EnrichSession updates the User & Email after the initial Redeem
141+
func (p *GitcodeProvider) EnrichSession(ctx context.Context, s *sessions.SessionState) error {
142+
p.Scope = gitcodeDefaultScope
143+
return p.getUser(ctx, s)
144+
}
145+
146+
func (p *GitcodeProvider) getUser(ctx context.Context, s *sessions.SessionState) error {
147+
user, err := p.userInfo(ctx, s.AccessToken)
148+
if err != nil {
149+
return err
150+
}
151+
// determine whether user is belong to org add code on this place
152+
// Now that we have the username we can check collaborator status
153+
if !p.isVerifiedUser(user.Login) && p.Org == "" && p.Repo != "" && p.Token != "" {
154+
if ok, err := p.isCollaborator(ctx, user.Login, p.Token); err != nil || !ok {
155+
return err
156+
}
157+
}
158+
159+
s.User = user.Login
160+
s.Email = user.Email
161+
return nil
162+
}
163+
164+
// RefreshSessionIfNeeded checks if the session has expired and uses the
165+
// RefreshToken to fetch a new ID token if required
166+
func (p *GitcodeProvider) RefreshSessionIfNeeded(ctx context.Context, s *sessions.SessionState) (bool, error) {
167+
if s == nil || (s.ExpiresOn != nil && s.ExpiresOn.After(time.Now())) || s.RefreshToken == "" {
168+
return false, nil
169+
}
170+
origExpiration := s.ExpiresOn
171+
172+
err := p.redeemRefreshToken(ctx, s)
173+
if err != nil {
174+
return false, fmt.Errorf("unable to redeem refresh token: %v", err)
175+
}
176+
177+
fmt.Printf("refreshed id token %s (expired on %s)\n", s, origExpiration)
178+
return true, nil
179+
}
180+
181+
func (p *GitcodeProvider) redeemRefreshToken(ctx context.Context, s *sessions.SessionState) error {
182+
params := url.Values{}
183+
params.Add("refresh_token", s.RefreshToken)
184+
params.Add("grant_type", "refresh_token")
185+
result := requests.New(p.RedeemURL.String()).
186+
WithContext(ctx).
187+
WithMethod("POST").
188+
WithBody(bytes.NewBufferString(params.Encode())).
189+
SetHeader("Content-Type", "application/x-www-form-urlencoded").
190+
Do()
191+
if result.Error() != nil {
192+
return result.Error()
193+
}
194+
fmt.Println(string(result.Body()))
195+
// blindly try json and x-www-form-urlencoded
196+
var jsonResponse struct {
197+
AccessToken string `json:"access_token"`
198+
ExpiresIn int64 `json:"expires_in"`
199+
RefreshToken string `json:"refresh_token"`
200+
Scope string `json:"scope"`
201+
CreateAt int64 `json:"create_at"`
202+
}
203+
err := result.UnmarshalInto(&jsonResponse)
204+
if err != nil {
205+
return err
206+
}
207+
expiresTime := jsonResponse.CreateAt + jsonResponse.ExpiresIn
208+
expiresOn := time.Unix(expiresTime, 0)
209+
ca := time.Unix(jsonResponse.CreateAt, 0)
210+
p.Scope = jsonResponse.Scope
211+
s.RefreshToken = jsonResponse.RefreshToken
212+
s.ExpiresOn = &expiresOn
213+
s.CreatedAt = &ca
214+
return nil
215+
}
216+
217+
func (p *GitcodeProvider) hasUser(ctx context.Context, accessToken string) (bool, error) {
218+
user, err := p.userInfo(ctx, accessToken)
219+
if err != nil {
220+
return false, err
221+
}
222+
if p.isVerifiedUser(user.Login) {
223+
return true, nil
224+
}
225+
return false, nil
226+
}
227+
228+
func (p *GitcodeProvider) userInfo(ctx context.Context, accessToken string) (userGitcode, error) {
229+
var user userGitcode
230+
endpoint := &url.URL{
231+
Scheme: p.ValidateURL.Scheme,
232+
Host: p.ValidateURL.Host,
233+
Path: path.Join(p.ValidateURL.Path, "/user"),
234+
}
235+
err := requests.New(endpoint.String()).
236+
WithContext(ctx).
237+
SetHeader("Authorization", "Bearer "+accessToken).
238+
SetHeader("Content-Type", "application/json;charset=UTF-8").
239+
Do().
240+
UnmarshalInto(&user)
241+
return user, err
242+
}
243+
244+
func (p *GitcodeProvider) isVerifiedUser(username string) bool {
245+
for _, u := range p.Users {
246+
if username == u {
247+
return true
248+
}
249+
}
250+
return false
251+
}
252+
253+
func (p *GitcodeProvider) isCollaborator(ctx context.Context, login string, accessToken string) (bool, error) {
254+
params := url.Values{}
255+
params.Add("access_token", accessToken)
256+
endpoint := &url.URL{
257+
Scheme: p.ValidateURL.Scheme,
258+
Host: p.ValidateURL.Host,
259+
Path: path.Join(p.ValidateURL.Path, "/repos/", p.Repo, "collaborators", login),
260+
RawQuery: params.Encode(),
261+
}
262+
result := requests.New(endpoint.String()).
263+
WithContext(ctx).
264+
SetHeader("Content-Type", "application/json;charset=UTF-8").
265+
Do()
266+
if result.Error() != nil {
267+
return false, result.Error()
268+
}
269+
270+
if result.StatusCode() != 204 {
271+
return false, fmt.Errorf("got %d from %q %s",
272+
result.StatusCode(), endpoint.String(), result.Body())
273+
}
274+
275+
logger.Printf("got %d from %q %s", result.StatusCode(), endpoint.String(), result.Body())
276+
277+
return true, nil
278+
279+
}
280+
281+
func (p *GitcodeProvider) hasOrg(ctx context.Context, accessToken string) (bool, error) {
282+
type orgsPage []struct {
283+
Login string `json:"login"`
284+
}
285+
286+
var orgs []struct {
287+
Login string `json:"login"`
288+
}
289+
290+
pn := 1
291+
ppg := 20
292+
params := url.Values{}
293+
params.Add("access_token", accessToken)
294+
for {
295+
params.Set("page", strconv.Itoa(pn))
296+
params.Set("per_page", strconv.Itoa(ppg))
297+
endpoint := &url.URL{
298+
Scheme: p.ValidateURL.Scheme,
299+
Host: p.ValidateURL.Host,
300+
Path: path.Join(p.ValidateURL.Path, "/user/orgs"),
301+
RawQuery: params.Encode(),
302+
}
303+
var op orgsPage
304+
err := requests.New(endpoint.String()).
305+
WithContext(ctx).
306+
SetHeader("Content-Type", "application/json;charset=UTF-8").
307+
Do().UnmarshalInto(&op)
308+
if err != nil {
309+
return false, err
310+
}
311+
if len(op) == 0 {
312+
break
313+
}
314+
orgs = append(orgs, op...)
315+
if len(op) < ppg {
316+
break
317+
}
318+
pn++
319+
}
320+
presentOrgs := make([]string, 0, len(orgs))
321+
for _, org := range orgs {
322+
if p.Org == org.Login {
323+
logger.Printf("found gitcode organization: %q", org.Login)
324+
return true, nil
325+
}
326+
presentOrgs = append(presentOrgs, org.Login)
327+
}
328+
329+
logger.Printf("missing organization:%q in %v", p.Org, presentOrgs)
330+
return false, nil
331+
}
332+
333+
func (p *GitcodeProvider) hasRepo(ctx context.Context, accessToken string) (bool, error) {
334+
type permissions struct {
335+
Pull bool `json:"pull"`
336+
Push bool `json:"push"`
337+
}
338+
339+
type repository struct {
340+
Permissions permissions `json:"permissions"`
341+
Private bool `json:"private"`
342+
}
343+
endpoint := &url.URL{
344+
Scheme: p.ValidateURL.Scheme,
345+
Host: p.ValidateURL.Host,
346+
Path: path.Join(p.ValidateURL.Path, "repos", p.Repo),
347+
}
348+
349+
var repo repository
350+
err := requests.New(endpoint.String()).
351+
WithContext(ctx).
352+
WithHeaders(makeGitHubHeader(accessToken)).
353+
Do().
354+
UnmarshalInto(&repo)
355+
if err != nil {
356+
return false, err
357+
}
358+
359+
// Every user can implicitly pull from a public repo, so only grant access
360+
// if they have push access or the repo is private and they can pull
361+
return repo.Permissions.Push || (repo.Private && repo.Permissions.Pull), nil
362+
}
363+
364+
// SetRepo configures the target repository and optional token to use
365+
func (p *GitcodeProvider) SetRepo(repo, token string) {
366+
p.Repo = repo
367+
p.Token = token
368+
}
369+
370+
// SetUsers configures allowed usernames
371+
func (p *GitcodeProvider) SetUsers(users []string) {
372+
p.Users = users
373+
}
374+
375+
// SetOrg adds Gitcode org reading parameters to the OAuth2 scope
376+
func (p *GitcodeProvider) SetOrg(org string) {
377+
p.Org = org
378+
if org != "" {
379+
p.Scope += " groups"
380+
}
381+
}

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)