Skip to content

Commit a2d97ab

Browse files
authored
improve gitlab token usage (#112)
* Removing test files * Adding support for both gitlab token styles
1 parent c9df808 commit a2d97ab

File tree

8 files changed

+500
-12
lines changed

8 files changed

+500
-12
lines changed

README.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,74 @@ The CLI uses intelligent default branch detection with the following priority:
179179
180180
Both `--default-branch` and `--pending-head` parameters are automatically synchronized to ensure consistent behavior.
181181
182+
## GitLab Token Configuration
183+
184+
The CLI supports GitLab integration with automatic authentication pattern detection for different token types.
185+
186+
### Supported Token Types
187+
188+
GitLab API supports two authentication methods, and the CLI automatically detects which one to use:
189+
190+
1. **Bearer Token Authentication** (`Authorization: Bearer <token>`)
191+
- GitLab CI Job Tokens (`$CI_JOB_TOKEN`)
192+
- Personal Access Tokens with `glpat-` prefix
193+
- OAuth 2.0 tokens (long alphanumeric tokens)
194+
195+
2. **Private Token Authentication** (`PRIVATE-TOKEN: <token>`)
196+
- Legacy personal access tokens
197+
- Custom tokens that don't match Bearer patterns
198+
199+
### Token Detection Logic
200+
201+
The CLI automatically determines the authentication method using this logic:
202+
203+
```
204+
if token == $CI_JOB_TOKEN:
205+
use Bearer authentication
206+
elif token starts with "glpat-":
207+
use Bearer authentication
208+
elif token is long (>40 chars) and alphanumeric:
209+
use Bearer authentication
210+
else:
211+
use PRIVATE-TOKEN authentication
212+
```
213+
214+
### Automatic Fallback
215+
216+
If the initial authentication method fails with a 401 error, the CLI automatically retries with the alternative method:
217+
218+
- **Bearer → PRIVATE-TOKEN**: If Bearer authentication fails, retry with PRIVATE-TOKEN
219+
- **PRIVATE-TOKEN → Bearer**: If PRIVATE-TOKEN fails, retry with Bearer authentication
220+
221+
This ensures maximum compatibility across different GitLab configurations and token types.
222+
223+
### Environment Variables
224+
225+
| Variable | Description | Example |
226+
|:---------|:------------|:--------|
227+
| `GITLAB_TOKEN` | GitLab API token (required for GitLab integration) | `glpat-xxxxxxxxxxxxxxxxxxxx` |
228+
| `CI_JOB_TOKEN` | GitLab CI job token (automatically used in GitLab CI) | Automatically provided by GitLab CI |
229+
230+
### Usage Examples
231+
232+
**GitLab CI with job token (recommended):**
233+
```yaml
234+
variables:
235+
GITLAB_TOKEN: $CI_JOB_TOKEN
236+
```
237+
238+
**GitLab CI with personal access token:**
239+
```yaml
240+
variables:
241+
GITLAB_TOKEN: $GITLAB_PERSONAL_ACCESS_TOKEN # Set in GitLab project/group variables
242+
```
243+
244+
**Local development:**
245+
```bash
246+
export GITLAB_TOKEN="glpat-your-personal-access-token"
247+
socketcli --integration gitlab --repo owner/repo --pr-number 123
248+
```
249+
182250
### Scan Behavior
183251
184252
The CLI determines scanning behavior intelligently:
@@ -340,4 +408,10 @@ Implementation targets:
340408
341409
### Environment Variables
342410
411+
#### Core Configuration
412+
- `SOCKET_SECURITY_API_KEY`: Socket Security API token (alternative to --api-token parameter)
343413
- `SOCKET_SDK_PATH`: Path to local socket-sdk-python repository (default: ../socket-sdk-python)
414+
415+
#### GitLab Integration
416+
- `GITLAB_TOKEN`: GitLab API token for GitLab integration (supports both Bearer and PRIVATE-TOKEN authentication)
417+
- `CI_JOB_TOKEN`: GitLab CI job token (automatically provided in GitLab CI environments)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
66

77
[project]
88
name = "socketsecurity"
9-
version = "2.2.0"
9+
version = "2.2.2"
1010
requires-python = ">= 3.10"
1111
license = {"file" = "LICENSE"}
1212
dependencies = [

socketsecurity/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
__author__ = 'socket.dev'
2-
__version__ = '2.2.0'
2+
__version__ = '2.2.2'

socketsecurity/core/scm/client.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,51 @@ def get_headers(self) -> Dict:
3434

3535
class GitlabClient(ScmClient):
3636
def get_headers(self) -> Dict:
37-
return {
38-
'Authorization': f"Bearer {self.token}",
37+
"""
38+
Determine the appropriate authentication headers for GitLab API.
39+
Uses the same logic as GitlabConfig._get_auth_headers()
40+
"""
41+
return self._get_gitlab_auth_headers(self.token)
42+
43+
@staticmethod
44+
def _get_gitlab_auth_headers(token: str) -> dict:
45+
"""
46+
Determine the appropriate authentication headers for GitLab API.
47+
48+
GitLab supports two authentication patterns:
49+
1. Bearer token (OAuth 2.0 tokens, personal access tokens with api scope)
50+
2. Private token (personal access tokens)
51+
"""
52+
import os
53+
54+
base_headers = {
3955
'User-Agent': 'SocketPythonScript/0.0.1',
4056
"accept": "application/json"
4157
}
58+
59+
# Check if this is a GitLab CI job token
60+
if token == os.getenv('CI_JOB_TOKEN'):
61+
return {
62+
**base_headers,
63+
'Authorization': f"Bearer {token}"
64+
}
65+
66+
# Check for personal access token pattern
67+
if token.startswith('glpat-'):
68+
return {
69+
**base_headers,
70+
'Authorization': f"Bearer {token}"
71+
}
72+
73+
# Check for OAuth token pattern (typically longer and alphanumeric)
74+
if len(token) > 40 and token.isalnum():
75+
return {
76+
**base_headers,
77+
'Authorization': f"Bearer {token}"
78+
}
79+
80+
# Default to PRIVATE-TOKEN for other token types
81+
return {
82+
**base_headers,
83+
'PRIVATE-TOKEN': f"{token}"
84+
}

socketsecurity/core/scm/gitlab.py

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ def from_env(cls) -> 'GitlabConfig':
4242
mr_source_branch = os.getenv('CI_MERGE_REQUEST_SOURCE_BRANCH_NAME')
4343
default_branch = os.getenv('CI_DEFAULT_BRANCH', '')
4444

45+
# Determine which authentication pattern to use
46+
headers = cls._get_auth_headers(token)
47+
4548
return cls(
4649
commit_sha=os.getenv('CI_COMMIT_SHA', ''),
4750
api_url=os.getenv('CI_API_V4_URL', ''),
@@ -57,18 +60,119 @@ def from_env(cls) -> 'GitlabConfig':
5760
token=token,
5861
repository=project_name,
5962
is_default_branch=(mr_source_branch == default_branch if mr_source_branch else False),
60-
headers={
61-
'Authorization': f"Bearer {token}",
62-
'User-Agent': 'SocketPythonScript/0.0.1',
63-
"accept": "application/json"
64-
}
63+
headers=headers
6564
)
6665

66+
@staticmethod
67+
def _get_auth_headers(token: str) -> dict:
68+
"""
69+
Determine the appropriate authentication headers for GitLab API.
70+
71+
GitLab supports two authentication patterns:
72+
1. Bearer token (OAuth 2.0 tokens, personal access tokens with api scope)
73+
2. Private token (personal access tokens)
74+
75+
Logic for token type determination:
76+
- CI_JOB_TOKEN: Always use Bearer (GitLab CI job token)
77+
- Tokens starting with 'glpat-': Personal access tokens, try Bearer first
78+
- OAuth tokens: Use Bearer
79+
- Other tokens: Use PRIVATE-TOKEN as fallback
80+
"""
81+
base_headers = {
82+
'User-Agent': 'SocketPythonScript/0.0.1',
83+
"accept": "application/json"
84+
}
85+
86+
# Check if this is a GitLab CI job token
87+
if token == os.getenv('CI_JOB_TOKEN'):
88+
log.debug("Using Bearer authentication for GitLab CI job token")
89+
return {
90+
**base_headers,
91+
'Authorization': f"Bearer {token}"
92+
}
93+
94+
# Check for personal access token pattern
95+
if token.startswith('glpat-'):
96+
log.debug("Using Bearer authentication for GitLab personal access token")
97+
return {
98+
**base_headers,
99+
'Authorization': f"Bearer {token}"
100+
}
101+
102+
# Check for OAuth token pattern (typically longer and alphanumeric)
103+
if len(token) > 40 and token.isalnum():
104+
log.debug("Using Bearer authentication for potential OAuth token")
105+
return {
106+
**base_headers,
107+
'Authorization': f"Bearer {token}"
108+
}
109+
110+
# Default to PRIVATE-TOKEN for other token types
111+
log.debug("Using PRIVATE-TOKEN authentication for GitLab token")
112+
return {
113+
**base_headers,
114+
'PRIVATE-TOKEN': f"{token}"
115+
}
116+
67117
class Gitlab:
68118
def __init__(self, client: CliClient, config: Optional[GitlabConfig] = None):
69119
self.config = config or GitlabConfig.from_env()
70120
self.client = client
71121

122+
def _request_with_fallback(self, **kwargs):
123+
"""
124+
Make a request with automatic fallback between Bearer and PRIVATE-TOKEN authentication.
125+
This provides robustness when the initial token type detection is incorrect.
126+
"""
127+
try:
128+
# Try the initial request with the configured headers
129+
return self.client.request(**kwargs)
130+
except Exception as e:
131+
# Check if this is an authentication error (401)
132+
if hasattr(e, 'response') and e.response and e.response.status_code == 401:
133+
log.debug(f"Authentication failed with initial headers, trying fallback method")
134+
135+
# Determine the fallback headers
136+
original_headers = kwargs.get('headers', self.config.headers)
137+
fallback_headers = self._get_fallback_headers(original_headers)
138+
139+
if fallback_headers and fallback_headers != original_headers:
140+
log.debug("Retrying request with fallback authentication method")
141+
kwargs['headers'] = fallback_headers
142+
return self.client.request(**kwargs)
143+
144+
# Re-raise the original exception if it's not an auth error or fallback failed
145+
raise
146+
147+
def _get_fallback_headers(self, original_headers: dict) -> dict:
148+
"""
149+
Generate fallback authentication headers.
150+
If using Bearer, fallback to PRIVATE-TOKEN and vice versa.
151+
"""
152+
base_headers = {
153+
'User-Agent': 'SocketPythonScript/0.0.1',
154+
"accept": "application/json"
155+
}
156+
157+
# If currently using Bearer, try PRIVATE-TOKEN
158+
if 'Authorization' in original_headers and 'Bearer' in original_headers['Authorization']:
159+
log.debug("Falling back from Bearer to PRIVATE-TOKEN authentication")
160+
return {
161+
**base_headers,
162+
'PRIVATE-TOKEN': f"{self.config.token}"
163+
}
164+
165+
# If currently using PRIVATE-TOKEN, try Bearer
166+
elif 'PRIVATE-TOKEN' in original_headers:
167+
log.debug("Falling back from PRIVATE-TOKEN to Bearer authentication")
168+
return {
169+
**base_headers,
170+
'Authorization': f"Bearer {self.config.token}"
171+
}
172+
173+
# No fallback available
174+
return None
175+
72176
def check_event_type(self) -> str:
73177
pipeline_source = self.config.pipeline_source.lower()
74178
if pipeline_source in ["web", 'merge_request_event', "push", "api"]:
@@ -84,7 +188,7 @@ def check_event_type(self) -> str:
84188
def post_comment(self, body: str) -> None:
85189
path = f"projects/{self.config.mr_project_id}/merge_requests/{self.config.mr_iid}/notes"
86190
payload = {"body": body}
87-
self.client.request(
191+
self._request_with_fallback(
88192
path=path,
89193
payload=payload,
90194
method="POST",
@@ -95,7 +199,7 @@ def post_comment(self, body: str) -> None:
95199
def update_comment(self, body: str, comment_id: str) -> None:
96200
path = f"projects/{self.config.mr_project_id}/merge_requests/{self.config.mr_iid}/notes/{comment_id}"
97201
payload = {"body": body}
98-
self.client.request(
202+
self._request_with_fallback(
99203
path=path,
100204
payload=payload,
101205
method="PUT",
@@ -106,7 +210,7 @@ def update_comment(self, body: str, comment_id: str) -> None:
106210
def get_comments_for_pr(self) -> dict:
107211
log.debug(f"Getting Gitlab comments for Repo {self.config.repository} for PR {self.config.mr_iid}")
108212
path = f"projects/{self.config.mr_project_id}/merge_requests/{self.config.mr_iid}/notes"
109-
response = self.client.request(
213+
response = self._request_with_fallback(
110214
path=path,
111215
headers=self.config.headers,
112216
base_url=self.config.api_url

0 commit comments

Comments
 (0)