From d497c33c7411569ca7e64cd53e476041fe98d18e Mon Sep 17 00:00:00 2001 From: Mr_Jing Date: Sun, 3 Aug 2025 08:46:41 +0800 Subject: [PATCH 1/4] fix: support private_token authentication for GitLab private deployments - Add intelligent authentication method detection based on GitLab URL - Support explicit configuration override via GITLAB.AUTH_TYPE - Maintain backward compatibility with existing oauth_token usage - Fix 401 Unauthorized errors for private GitLab deployments Fixes authentication issues where private GitLab instances require private_token instead of oauth_token for API access. --- pr_agent/git_providers/gitlab_provider.py | 40 ++++++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index 8cfe05f6..ac889b62 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -35,10 +35,20 @@ class GitLabProvider(GitProvider): gitlab_access_token = get_settings().get("GITLAB.PERSONAL_ACCESS_TOKEN", None) if not gitlab_access_token: raise ValueError("GitLab personal access token is not set in the config file") - self.gl = gitlab.Gitlab( - url=gitlab_url, - oauth_token=gitlab_access_token - ) + # Use encapsulated method to determine authentication method + auth_method = self._get_auth_method(gitlab_url) + + # Create GitLab instance based on authentication method + if auth_method == "oauth_token": + self.gl = gitlab.Gitlab( + url=gitlab_url, + oauth_token=gitlab_access_token + ) + else: # private_token + self.gl = gitlab.Gitlab( + url=gitlab_url, + private_token=gitlab_access_token + ) self.max_comment_chars = 65000 self.id_project = None self.id_mr = None @@ -52,6 +62,26 @@ class GitLabProvider(GitProvider): r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)") self.incremental = incremental + def _get_auth_method(self, gitlab_url: str) -> str: + """ + Determine the authentication method for a GitLab instance. + + Args: + gitlab_url: URL of the GitLab instance + + Returns: + Authentication method: "oauth_token" or "private_token" + """ + # Check for explicit configuration override first + explicit_auth_type = get_settings().get("GITLAB.AUTH_TYPE", None) + if explicit_auth_type: + return explicit_auth_type + + # Default strategy: gitlab.com and gitlab.io use oauth_token, others use private_token + if "gitlab.com" in gitlab_url or "gitlab.io" in gitlab_url: + return "oauth_token" + return "private_token" + def is_supported(self, capability: str) -> bool: if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments', 'publish_file_comments']: # gfm_markdown is supported in gitlab ! @@ -677,7 +707,7 @@ class GitLabProvider(GitProvider): get_logger().error(f"Repo URL: {repo_url_to_clone} is not a valid gitlab URL.") return None (scheme, base_url) = repo_url_to_clone.split("gitlab.") - access_token = self.gl.oauth_token + access_token = self.gl.oauth_token or self.gl.private_token if not all([scheme, access_token, base_url]): get_logger().error(f"Either no access token found, or repo URL: {repo_url_to_clone} " f"is missing prefix: {scheme} and/or base URL: {base_url}.") From 2d858a43be41be4a08dde95669cbfc017dcd9a72 Mon Sep 17 00:00:00 2001 From: Mr_Jing Date: Sun, 3 Aug 2025 09:58:59 +0800 Subject: [PATCH 2/4] refactor: improve GitLab authentication method detection Address code review feedback from PR #1969: 1. Improve URL matching precision: - Use urlparse for robust hostname validation - Prevent false positives with URL substring matching - Add support for gitlab.com/gitlab.io subdomains 2. Add authentication type validation: - Validate explicit GITLAB.AUTH_TYPE configuration - Provide clear error messages for invalid auth types - Prevent silent failures from user configuration errors This enhances code reliability and user experience while maintaining backward compatibility. --- pr_agent/git_providers/gitlab_provider.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index ac889b62..82287fcf 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -75,11 +75,23 @@ class GitLabProvider(GitProvider): # Check for explicit configuration override first explicit_auth_type = get_settings().get("GITLAB.AUTH_TYPE", None) if explicit_auth_type: + # Validate the explicit authentication type + if explicit_auth_type not in ["oauth_token", "private_token"]: + raise ValueError(f"Unsupported GITLAB.AUTH_TYPE: '{explicit_auth_type}'. " + f"Must be 'oauth_token' or 'private_token'.") return explicit_auth_type - # Default strategy: gitlab.com and gitlab.io use oauth_token, others use private_token - if "gitlab.com" in gitlab_url or "gitlab.io" in gitlab_url: - return "oauth_token" + # Default strategy: Use precise hostname matching for gitlab.com and gitlab.io + try: + parsed_url = urlparse(gitlab_url) + hostname = parsed_url.hostname + if hostname and (hostname == "gitlab.com" or hostname == "gitlab.io" or + hostname.endswith(".gitlab.com") or hostname.endswith(".gitlab.io")): + return "oauth_token" + except Exception: + # If URL parsing fails, fall back to private_token for safety + pass + return "private_token" def is_supported(self, capability: str) -> bool: From dd6f56915bf7f5f6dbb6b196a6f15260185fa796 Mon Sep 17 00:00:00 2001 From: Mr_Jing Date: Mon, 4 Aug 2025 10:35:52 +0800 Subject: [PATCH 3/4] improve: enhance GitLab provider error handling and attribute access safety - Add try-catch block around GitLab instance creation for better error handling - Use getattr() for safer attribute access in _prepare_clone_url_with_token method - Improve authentication failure debugging with clearer error messages --- pr_agent/git_providers/gitlab_provider.py | 26 +++++++++++++---------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index 82287fcf..e172106f 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -39,16 +39,20 @@ class GitLabProvider(GitProvider): auth_method = self._get_auth_method(gitlab_url) # Create GitLab instance based on authentication method - if auth_method == "oauth_token": - self.gl = gitlab.Gitlab( - url=gitlab_url, - oauth_token=gitlab_access_token - ) - else: # private_token - self.gl = gitlab.Gitlab( - url=gitlab_url, - private_token=gitlab_access_token - ) + try: + if auth_method == "oauth_token": + self.gl = gitlab.Gitlab( + url=gitlab_url, + oauth_token=gitlab_access_token + ) + else: # private_token + self.gl = gitlab.Gitlab( + url=gitlab_url, + private_token=gitlab_access_token + ) + except Exception as e: + get_logger().error(f"Failed to create GitLab instance: {e}") + raise ValueError(f"Unable to authenticate with GitLab: {e}") self.max_comment_chars = 65000 self.id_project = None self.id_mr = None @@ -719,7 +723,7 @@ class GitLabProvider(GitProvider): get_logger().error(f"Repo URL: {repo_url_to_clone} is not a valid gitlab URL.") return None (scheme, base_url) = repo_url_to_clone.split("gitlab.") - access_token = self.gl.oauth_token or self.gl.private_token + access_token = getattr(self.gl, 'oauth_token', None) or getattr(self.gl, 'private_token', None) if not all([scheme, access_token, base_url]): get_logger().error(f"Either no access token found, or repo URL: {repo_url_to_clone} " f"is missing prefix: {scheme} and/or base URL: {base_url}.") From fb73eb75f900718adb5af82017fb89b14e1185e9 Mon Sep 17 00:00:00 2001 From: Mr_Jing Date: Wed, 6 Aug 2025 12:03:53 +0800 Subject: [PATCH 4/4] refactor: simplify GitLab authentication configuration --- docs/docs/installation/gitlab.md | 2 ++ pr_agent/git_providers/gitlab_provider.py | 40 ++++------------------- 2 files changed, 9 insertions(+), 33 deletions(-) diff --git a/docs/docs/installation/gitlab.md b/docs/docs/installation/gitlab.md index 7e587617..da54848e 100644 --- a/docs/docs/installation/gitlab.md +++ b/docs/docs/installation/gitlab.md @@ -67,6 +67,7 @@ git clone https://github.com/qodo-ai/pr-agent.git 2. In the secrets file/variables: - Set your AI model key in the respective section - In the [gitlab] section, set `personal_access_token` (with token from step 2) and `shared_secret` (with secret from step 3) + - **Authentication type**: Set `auth_type` to `"private_token"` for older GitLab versions (e.g., 11.x) or private deployments. Default is `"oauth_token"` for gitlab.com and newer versions. 6. Build a Docker image for the app and optionally push it to a Docker repository. We'll use Dockerhub as an example: @@ -82,6 +83,7 @@ CONFIG__GIT_PROVIDER=gitlab GITLAB__PERSONAL_ACCESS_TOKEN= GITLAB__SHARED_SECRET= GITLAB__URL=https://gitlab.com +GITLAB__AUTH_TYPE=oauth_token # Use "private_token" for older GitLab versions OPENAI__KEY= ``` diff --git a/pr_agent/git_providers/gitlab_provider.py b/pr_agent/git_providers/gitlab_provider.py index e172106f..c75e2a8a 100644 --- a/pr_agent/git_providers/gitlab_provider.py +++ b/pr_agent/git_providers/gitlab_provider.py @@ -35,8 +35,13 @@ class GitLabProvider(GitProvider): gitlab_access_token = get_settings().get("GITLAB.PERSONAL_ACCESS_TOKEN", None) if not gitlab_access_token: raise ValueError("GitLab personal access token is not set in the config file") - # Use encapsulated method to determine authentication method - auth_method = self._get_auth_method(gitlab_url) + # Authentication method selection via configuration + auth_method = get_settings().get("GITLAB.AUTH_TYPE", "oauth_token") + + # Basic validation of authentication type + if auth_method not in ["oauth_token", "private_token"]: + raise ValueError(f"Unsupported GITLAB.AUTH_TYPE: '{auth_method}'. " + f"Must be 'oauth_token' or 'private_token'.") # Create GitLab instance based on authentication method try: @@ -66,37 +71,6 @@ class GitLabProvider(GitProvider): r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@[ ]?(.*)") self.incremental = incremental - def _get_auth_method(self, gitlab_url: str) -> str: - """ - Determine the authentication method for a GitLab instance. - - Args: - gitlab_url: URL of the GitLab instance - - Returns: - Authentication method: "oauth_token" or "private_token" - """ - # Check for explicit configuration override first - explicit_auth_type = get_settings().get("GITLAB.AUTH_TYPE", None) - if explicit_auth_type: - # Validate the explicit authentication type - if explicit_auth_type not in ["oauth_token", "private_token"]: - raise ValueError(f"Unsupported GITLAB.AUTH_TYPE: '{explicit_auth_type}'. " - f"Must be 'oauth_token' or 'private_token'.") - return explicit_auth_type - - # Default strategy: Use precise hostname matching for gitlab.com and gitlab.io - try: - parsed_url = urlparse(gitlab_url) - hostname = parsed_url.hostname - if hostname and (hostname == "gitlab.com" or hostname == "gitlab.io" or - hostname.endswith(".gitlab.com") or hostname.endswith(".gitlab.io")): - return "oauth_token" - except Exception: - # If URL parsing fails, fall back to private_token for safety - pass - - return "private_token" def is_supported(self, capability: str) -> bool: if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments',