Skip to content

Commit 77ede8a

Browse files
Fix projects API
Signed-off-by: Lukasz Gryglicki <lgryglicki@cncf.io> Assisted by [OpenAI](https://platform.openai.com/) Assisted by [GitHub Copilot](https://github.com/features/copilot)
1 parent 74a6b29 commit 77ede8a

3 files changed

Lines changed: 103 additions & 48 deletions

File tree

cla-backend-legacy/internal/api/handlers.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5362,15 +5362,15 @@ func (h *Handlers) GetProjectV2(w http.ResponseWriter, r *http.Request) {
53625362
standalone := false
53635363
lfSupported := false
53645364
if projectSFID != "" && h.salesforce != nil {
5365-
standalone, err = h.salesforce.IsStandaloneProject(ctx, projectSFID)
5366-
if err != nil {
5367-
respond.JSON(w, http.StatusInternalServerError, map[string]any{"errors": map[string]any{"project_id": err.Error()}})
5368-
return
5365+
if v, psErr := h.salesforce.IsStandaloneProject(ctx, projectSFID); psErr != nil {
5366+
logging.Warnf("get_project: unable to compute standalone_project for project_sfid=%s: %v", projectSFID, psErr)
5367+
} else {
5368+
standalone = v
53695369
}
5370-
lfSupported, err = h.salesforce.IsLFSupportedProject(ctx, projectSFID)
5371-
if err != nil {
5372-
respond.JSON(w, http.StatusInternalServerError, map[string]any{"errors": map[string]any{"project_id": err.Error()}})
5373-
return
5370+
if v, psErr := h.salesforce.IsLFSupportedProject(ctx, projectSFID); psErr != nil {
5371+
logging.Warnf("get_project: unable to compute lf_supported for project_sfid=%s: %v", projectSFID, psErr)
5372+
} else {
5373+
lfSupported = v
53745374
}
53755375
}
53765376
md["standalone_project"] = standalone

cla-backend-legacy/internal/legacy/salesforce/service.go

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -326,15 +326,20 @@ func (s *Service) getAccessToken(ctx context.Context) (string, int, error) {
326326
func (s *Service) IsStandaloneProject(ctx context.Context, projectSFID string) (bool, error) {
327327
project, err := s.getProjectDetailByID(ctx, projectSFID)
328328
if err != nil {
329-
return false, err
329+
//return false, err
330+
// Python ProjectService.get_project_by_id() returns None on downstream
331+
// project-service HTTP errors; is_standalone() then returns False.
332+
return false, nil
330333
}
331334
if project == nil {
332335
return false, nil
333336
}
334337

335338
parentName := s.getParentName(project)
336339
if parentName == nil {
337-
return false, nil
340+
//return false, nil
341+
// Python: parent_name is None => standalone.
342+
return true, nil
338343
}
339344
if *parentName == TheLinuxFoundation || *parentName == LFProjectsLLC {
340345
if len(project.Projects) == 0 {
@@ -350,7 +355,10 @@ func (s *Service) IsStandaloneProject(ctx context.Context, projectSFID string) (
350355
func (s *Service) IsLFSupportedProject(ctx context.Context, projectSFID string) (bool, error) {
351356
project, err := s.getProjectDetailByID(ctx, projectSFID)
352357
if err != nil {
353-
return false, err
358+
//return false, err
359+
// Python ProjectService.get_project_by_id() returns None on downstream
360+
// project-service HTTP errors; is_lf_supported() then returns False.
361+
return false, nil
354362
}
355363
if project == nil {
356364
return false, nil
@@ -379,13 +387,19 @@ func (s *Service) getProjectDetailByID(ctx context.Context, projectID string) (*
379387
return nil, &AuthFailureError{Status: status, Cause: err}
380388
}
381389

382-
// Use the same endpoint as GetProject but with a different struct for full details
383-
projectURL := fmt.Sprintf("%s/project-service/v2/projects/%s", s.platformGatewayURL, url.QueryEscape(projectID))
390+
base := strings.TrimRight(s.platformGatewayURL, "/")
391+
if base == "" {
392+
return nil, errors.New("PLATFORM_GATEWAY_URL is empty")
393+
}
394+
395+
// Legacy Python uses /project-service/v1/projects/{project_id}.
396+
projectURL := fmt.Sprintf("%s/project-service/v1/projects/%s", base, url.PathEscape(projectID))
384397
req, err := http.NewRequestWithContext(ctx, "GET", projectURL, nil)
385398
if err != nil {
386399
return nil, err
387400
}
388-
req.Header.Set("Authorization", "Bearer "+accessToken)
401+
// req.Header.Set("Authorization", "Bearer "+accessToken)
402+
req.Header.Set("Authorization", "bearer "+accessToken)
389403
req.Header.Set("Accept", "application/json")
390404

391405
resp, err := s.httpClient.Do(req)
@@ -395,11 +409,9 @@ func (s *Service) getProjectDetailByID(ctx context.Context, projectID string) (*
395409
defer resp.Body.Close()
396410

397411
if resp.StatusCode != http.StatusOK {
398-
body, _ := io.ReadAll(io.LimitReader(resp.Body, 8192))
399-
return nil, &ProjectServiceError{
400-
Status: resp.StatusCode,
401-
Cause: fmt.Errorf("project-service GET project status %d: %s", resp.StatusCode, string(body)),
402-
}
412+
// Legacy Python catches HTTPError and returns None.
413+
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 8192))
414+
return nil, nil
403415
}
404416

405417
var project ProjectDetail

cla-backend-legacy/internal/middleware/cors.go

Lines changed: 72 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,47 +6,90 @@ package middleware
66
import (
77
"encoding/json"
88
"net/http"
9+
"net/url"
910
"os"
1011
"strings"
1112
"sync"
1213
)
1314

1415
// CORS is a simple "always add CORS headers" middleware.
1516
// The legacy Python backend sets these headers in response middleware.
16-
1717
var (
1818
allowedOriginsOnce sync.Once
1919
allowedOrigins []string
2020
allowAllOrigins bool
2121
)
2222

23+
func normalizeAllowedOrigin(raw string) string {
24+
raw = strings.TrimSpace(strings.Trim(raw, "\"'"))
25+
if raw == "" || raw == "*" {
26+
return raw
27+
}
28+
29+
// CLA_CONTRIBUTOR_BASE / CLA_CONTRIBUTOR_V2_BASE can be configured as a
30+
// hostname. Browser Origin values are scheme + host only.
31+
if !strings.Contains(raw, "://") {
32+
raw = "https://" + raw
33+
}
34+
35+
u, err := url.Parse(raw)
36+
if err == nil && u.Scheme != "" && u.Host != "" {
37+
return u.Scheme + "://" + u.Host
38+
}
39+
40+
return strings.TrimRight(raw, "/")
41+
}
42+
43+
func addAllowedOrigin(raw string) {
44+
origin := normalizeAllowedOrigin(raw)
45+
if origin == "" {
46+
return
47+
}
48+
if origin == "*" {
49+
allowAllOrigins = true
50+
return
51+
}
52+
53+
for _, existing := range allowedOrigins {
54+
if existing == origin {
55+
return
56+
}
57+
}
58+
allowedOrigins = append(allowedOrigins, origin)
59+
}
60+
61+
func addContributorConsoleOrigins() {
62+
addAllowedOrigin(os.Getenv("CLA_CONTRIBUTOR_BASE"))
63+
addAllowedOrigin(os.Getenv("CLA_CONTRIBUTOR_V2_BASE"))
64+
}
65+
2366
func loadAllowedOriginsFromEnv() {
2467
raw := strings.TrimSpace(os.Getenv("ALLOWED_ORIGINS"))
2568
if raw == "" {
2669
// Backwards compatible default: allow all.
2770
allowAllOrigins = true
71+
addContributorConsoleOrigins()
2872
return
2973
}
74+
3075
// Supported formats:
31-
// - JSON array: ["https://a", "https://b"]
32-
// - CSV: https://a,https://b
33-
// - Space/newline separated
76+
// - JSON array: ["https://a", "https://b"]
77+
// - CSV: https://a,https://b
78+
// - Space/newline separated
3479
if strings.HasPrefix(raw, "[") {
3580
var arr []string
3681
if err := json.Unmarshal([]byte(raw), &arr); err == nil {
3782
for _, v := range arr {
38-
v = strings.TrimSpace(strings.Trim(v, "\"'"))
39-
if v == "" {
40-
continue
41-
}
42-
allowedOrigins = append(allowedOrigins, v)
43-
if v == "*" {
44-
allowAllOrigins = true
45-
}
83+
addAllowedOrigin(v)
84+
}
85+
addContributorConsoleOrigins()
86+
if len(allowedOrigins) == 0 && !allowAllOrigins {
87+
allowAllOrigins = true
4688
}
4789
return
4890
}
4991
}
92+
5093
parts := strings.FieldsFunc(raw, func(r rune) bool {
5194
switch r {
5295
case ',', ';', ' ', '\n', '\t', '\r':
@@ -56,16 +99,15 @@ func loadAllowedOriginsFromEnv() {
5699
}
57100
})
58101
for _, p := range parts {
59-
p = strings.TrimSpace(strings.Trim(p, "\"'"))
60-
if p == "" {
61-
continue
62-
}
63-
allowedOrigins = append(allowedOrigins, p)
64-
if p == "*" {
65-
allowAllOrigins = true
66-
}
102+
addAllowedOrigin(p)
67103
}
68-
if len(allowedOrigins) == 0 {
104+
105+
// The legacy GitHub signing flow redirects contributors to these consoles.
106+
// Therefore these origins must be allowed to call the v1/v2 APIs after
107+
// GitHub OAuth redirects back to EasyCLA.
108+
addContributorConsoleOrigins()
109+
110+
if len(allowedOrigins) == 0 && !allowAllOrigins {
69111
allowAllOrigins = true
70112
}
71113
}
@@ -75,10 +117,12 @@ func isOriginAllowed(origin string) bool {
75117
if allowAllOrigins {
76118
return true
77119
}
78-
origin = strings.TrimSpace(origin)
120+
121+
origin = normalizeAllowedOrigin(origin)
79122
if origin == "" {
80123
return false
81124
}
125+
82126
for _, o := range allowedOrigins {
83127
if origin == o {
84128
return true
@@ -89,22 +133,21 @@ func isOriginAllowed(origin string) bool {
89133

90134
func CORS(next http.Handler) http.Handler {
91135
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
92-
origin := r.Header.Get("Origin")
136+
allowedOriginsOnce.Do(loadAllowedOriginsFromEnv)
137+
138+
origin := normalizeAllowedOrigin(r.Header.Get("Origin"))
139+
93140
if origin != "" && isOriginAllowed(origin) {
94-
// Echo the origin when allowlisting is enabled.
141+
// Echo the origin. Browsers reject "*" when credentials/cookies are used.
95142
w.Header().Set("Access-Control-Allow-Origin", origin)
96143
w.Header().Add("Vary", "Origin")
97144
} else if origin == "" && isOriginAllowed("*") {
98145
// Non-browser clients.
99146
w.Header().Set("Access-Control-Allow-Origin", "*")
100-
} else if origin != "" && isOriginAllowed("*") {
101-
// Backwards compatible default: allow all.
102-
w.Header().Set("Access-Control-Allow-Origin", "*")
103147
}
148+
104149
// Legacy Python sets the string literal "true".
105150
w.Header().Set("Access-Control-Allow-Credentials", "true")
106-
// Keep this list *exactly* aligned with the legacy Python middleware:
107-
// response.set_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
108151
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
109152
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
110153

0 commit comments

Comments
 (0)