diff --git a/components/backend/handlers/gitlab_auth.go b/components/backend/handlers/gitlab_auth.go index bb47a1f57..7b165e68a 100644 --- a/components/backend/handlers/gitlab_auth.go +++ b/components/backend/handlers/gitlab_auth.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "net" "net/http" "net/url" "strings" @@ -18,6 +19,8 @@ import ( "ambient-code-backend/gitlab" ) +var validateGitLabConnectivityFn = validateGitLabConnectivity + // GitLabAuthHandler handles GitLab authentication endpoints type GitLabAuthHandler struct { connectionManager *gitlab.ConnectionManager @@ -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 @@ -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 + } + // Store credentials at cluster level creds := &GitLabCredentials{ UserID: userID, diff --git a/components/backend/handlers/gitlab_auth_test.go b/components/backend/handlers/gitlab_auth_test.go index 1cbe2bb58..c110be234 100644 --- a/components/backend/handlers/gitlab_auth_test.go +++ b/components/backend/handlers/gitlab_auth_test.go @@ -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) @@ -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 { @@ -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() {