Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions components/backend/handlers/gitlab_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/url"
"strings"
Expand All @@ -18,6 +19,8 @@ import (
"ambient-code-backend/gitlab"
)

var validateGitLabConnectivityFn = validateGitLabConnectivity

// GitLabAuthHandler handles GitLab authentication endpoints
type GitLabAuthHandler struct {
connectionManager *gitlab.ConnectionManager
Expand Down Expand Up @@ -115,6 +118,40 @@ func validateGitLabInput(instanceURL, token string) error {
return nil
}

// validateGitLabConnectivity validates that the instance is resolvable and PAT can authenticate.
func validateGitLabConnectivity(ctx context.Context, instanceURL, token string) error {
parsedURL, err := url.Parse(instanceURL)
if err != nil {
return fmt.Errorf("invalid instance URL format")
}

hostname := parsedURL.Hostname()
if hostname == "" {
return fmt.Errorf("instance URL must have a valid hostname")
}

resolveCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

addresses, err := net.DefaultResolver.LookupHost(resolveCtx, hostname)
if err != nil {
return fmt.Errorf("cannot resolve GitLab instance host '%s': %w", hostname, err)
}
if len(addresses) == 0 {
return fmt.Errorf("cannot resolve GitLab instance host '%s': no addresses returned", hostname)
}

valid, err := ValidateGitLabToken(ctx, token, instanceURL)
if err != nil {
return fmt.Errorf("failed to connect to GitLab API at %s: %w", instanceURL, err)
}
if !valid {
return fmt.Errorf("failed to authenticate with the provided Personal Access Token")
}

return nil
}

// ConnectGitLab handles POST /projects/:projectName/auth/gitlab/connect
func (h *GitLabAuthHandler) ConnectGitLab(c *gin.Context) {
// Get project from URL parameter
Expand Down Expand Up @@ -410,6 +447,12 @@ func ConnectGitLabGlobal(c *gin.Context) {
return
}

// Validate connectivity/auth before storing credentials.
if err := validateGitLabConnectivityFn(c.Request.Context(), req.InstanceURL, req.PersonalAccessToken); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("GitLab connectivity check failed: %v", err)})
return
}
Comment on lines +450 to +454
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Connectivity validation missing in project-scoped handler.

ConnectGitLabGlobal now validates connectivity before storing credentials, but the project-scoped ConnectGitLab handler (lines 156-248) does not perform the same validation. This inconsistency means project-scoped integrations can still save unreachable or invalid GitLab connections.

🐛 Suggested fix: Add connectivity check to ConnectGitLab

Add the connectivity check in ConnectGitLab after input validation (around line 188):

 	// Validate input
 	if err := validateGitLabInput(req.InstanceURL, req.PersonalAccessToken); err != nil {
 		c.JSON(http.StatusBadRequest, gin.H{
 			"error":      fmt.Sprintf("Invalid input: %v", err),
 			"statusCode": http.StatusBadRequest,
 		})
 		return
 	}
+
+	// Validate connectivity/auth before storing credentials.
+	if err := validateGitLabConnectivityFn(c.Request.Context(), req.InstanceURL, req.PersonalAccessToken); err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{
+			"error":      fmt.Sprintf("GitLab connectivity check failed: %v", err),
+			"statusCode": http.StatusBadRequest,
+		})
+		return
+	}

 	// Get user ID from context (set by authentication middleware)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/backend/handlers/gitlab_auth.go` around lines 450 - 454, The
project-scoped handler ConnectGitLab is missing the GitLab connectivity/auth
validation that ConnectGitLabGlobal performs; after ConnectGitLab's input
validation step, call validateGitLabConnectivityFn with the request context,
req.InstanceURL, and req.PersonalAccessToken, and if it returns an error respond
with c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("GitLab
connectivity check failed: %v", err)}) and return — mirror the same logic used
in ConnectGitLabGlobal to prevent saving invalid/unreachable credentials.


// Store credentials at cluster level
creds := &GitLabCredentials{
UserID: userID,
Expand Down
52 changes: 48 additions & 4 deletions components/backend/handlers/gitlab_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,19 @@ import (

var _ = Describe("GitLab Auth Handler", Label(test_constants.LabelUnit, test_constants.LabelHandlers, test_constants.LabelGitLabAuth), func() {
var (
httpUtils *test_utils.HTTPTestUtils
k8sUtils *test_utils.K8sTestUtils
originalNamespace string
testToken string
httpUtils *test_utils.HTTPTestUtils
k8sUtils *test_utils.K8sTestUtils
originalNamespace string
originalValidateGitLabConnectivity func(context.Context, string, string) error
testToken string
)

BeforeEach(func() {
logger.Log("Setting up GitLab Auth Handler test")

originalNamespace = Namespace
originalValidateGitLabConnectivity = validateGitLabConnectivityFn
validateGitLabConnectivityFn = func(_ context.Context, _, _ string) error { return nil }

// Use centralized handler dependencies setup
k8sUtils = test_utils.NewK8sTestUtils(false, *config.TestNamespace)
Expand Down Expand Up @@ -67,6 +70,7 @@ var _ = Describe("GitLab Auth Handler", Label(test_constants.LabelUnit, test_con

AfterEach(func() {
Namespace = originalNamespace
validateGitLabConnectivityFn = originalValidateGitLabConnectivity

// Clean up created namespace (best-effort)
if k8sUtils != nil {
Expand Down Expand Up @@ -566,6 +570,46 @@ var _ = Describe("GitLab Auth Handler", Label(test_constants.LabelUnit, test_con

// Note: Global function K8s client validation tested at integration level
// Unit tests focus on specific handler logic

It("Should fail when GitLab hostname cannot be resolved", func() {
validateGitLabConnectivityFn = func(_ context.Context, _, _ string) error {
return fmt.Errorf("cannot resolve GitLab instance host 'gitlab.internal': lookup failed")
}

requestBody := map[string]interface{}{
"personalAccessToken": "valid_token_1234567890",
"instanceUrl": "https://gitlab.internal",
}

context := httpUtils.CreateTestGinContext("POST", "/auth/gitlab/connect", requestBody)
httpUtils.SetAuthHeader(testToken)
httpUtils.SetUserContext("test-user", "Test User", "test@example.com")

ConnectGitLabGlobal(context)

httpUtils.AssertHTTPStatus(http.StatusBadRequest)
httpUtils.AssertErrorMessage("GitLab connectivity check failed: cannot resolve GitLab instance host 'gitlab.internal': lookup failed")
})

It("Should fail when GitLab PAT authentication fails", func() {
validateGitLabConnectivityFn = func(_ context.Context, _, _ string) error {
return fmt.Errorf("failed to authenticate with the provided Personal Access Token")
}

requestBody := map[string]interface{}{
"personalAccessToken": "valid_token_1234567890",
"instanceUrl": "https://gitlab.com",
}

context := httpUtils.CreateTestGinContext("POST", "/auth/gitlab/connect", requestBody)
httpUtils.SetAuthHeader(testToken)
httpUtils.SetUserContext("test-user", "Test User", "test@example.com")

ConnectGitLabGlobal(context)

httpUtils.AssertHTTPStatus(http.StatusBadRequest)
httpUtils.AssertErrorMessage("GitLab connectivity check failed: failed to authenticate with the provided Personal Access Token")
})
})

Describe("GetGitLabStatusGlobal", func() {
Expand Down
Loading