8
8
import json
9
9
import logging
10
10
import time
11
+ import fnmatch
11
12
from collections import defaultdict
12
13
from typing import Any , Dict , List , Optional
13
14
import asyncio
@@ -28,8 +29,20 @@ class GitLabConfig:
28
29
def __init__ (self ):
29
30
# While using a client like cursor limit the api calls, actions & repos
30
31
self .max_api_calls_per_hour = int (os .getenv ("MCP_MAX_API_CALLS" , "100" ))
31
- self .allowed_actions = os .getenv ("MCP_ALLOWED_ACTIONS" , "read" ).split ("," )
32
- self .allowed_repos = os .getenv ("MCP_ALLOWED_REPOS" , "" ).split ("," ) if os .getenv ("MCP_ALLOWED_REPOS" ) else []
32
+
33
+ # CRITICAL: Hardcoded read-only actions for security - no configuration allowed
34
+ # For security, we only allow read-only queries - write operations are never permitted
35
+ self .allowed_actions = ['read' , 'list' , 'search' , 'get' , 'check' , 'info' , 'describe' , 'find' ]
36
+
37
+ # Parse MCP_ALLOWED_REPOS with support for wildcards: repo1,group1/*,group2/subgroup/*,exact-project
38
+ # This is now a REQUIRED environment variable
39
+ allowed_repos_env = os .getenv ("MCP_ALLOWED_REPOS" )
40
+ if not allowed_repos_env :
41
+ raise ValueError ("MCP_ALLOWED_REPOS is required. Set repository patterns like 'automotive/*,project1,group/subgroup/*'" )
42
+
43
+ self .allowed_repos = [repo .strip () for repo in allowed_repos_env .split ("," ) if repo .strip ()]
44
+ if not self .allowed_repos :
45
+ raise ValueError ("MCP_ALLOWED_REPOS cannot be empty. Specify at least one repository pattern." )
33
46
34
47
# Rate limiting tracking
35
48
self .api_calls = defaultdict (list )
@@ -42,13 +55,33 @@ def __init__(self):
42
55
if not self .gitlab_token :
43
56
raise ValueError ("GitLab token required. Set MCP_GITLAB_TOKEN environment variable" )
44
57
58
+ # SSL Configuration options
59
+ ssl_verify = os .getenv ("MCP_SSL_VERIFY" , "true" ).lower () == "true"
60
+ ssl_ca_bundle = os .getenv ("MCP_SSL_CA_BUNDLE" ) # Path to CA bundle file
61
+
62
+ # Build GitLab client configuration
63
+ gitlab_config = {
64
+ "url" : self .gitlab_url ,
65
+ "private_token" : self .gitlab_token ,
66
+ "ssl_verify" : ssl_verify
67
+ }
68
+
69
+ # Add CA bundle if provided
70
+ if ssl_ca_bundle and ssl_verify :
71
+ gitlab_config ["ssl_ca_bundle" ] = ssl_ca_bundle
72
+ logger .info (f"🔒 Using SSL CA bundle: { ssl_ca_bundle } " )
73
+ elif not ssl_verify :
74
+ logger .warning (f"⚠️ SSL verification disabled - not recommended for production!" )
75
+
45
76
# Initialize GitLab client
46
- self .gl = gitlab .Gitlab (self . gitlab_url , private_token = self . gitlab_token )
77
+ self .gl = gitlab .Gitlab (** gitlab_config )
47
78
48
79
# Log configuration
49
80
logger .info (f"🔒 Max API calls/hour: { self .max_api_calls_per_hour } " )
50
- logger .info (f"🔒 Allowed actions: { self .allowed_actions } " )
51
- logger .info (f"🔒 Allowed repos: { self .allowed_repos if self .allowed_repos != ['' ] else 'ALL' } " )
81
+ logger .info (f"🔒🛡️ SECURITY: Read-only mode hardcoded (write operations never permitted)" )
82
+ logger .info (f"ℹ️ Allowed actions: { self .allowed_actions } " )
83
+ logger .info (f"🔒 Allowed repos (wildcard patterns): { self .allowed_repos } " )
84
+ logger .info (f"ℹ️ Pattern examples: 'repo-name', 'group/*', 'group/subgroup/*', 'group/project'" )
52
85
logger .info (f"🔗 GitLab URL: { self .gitlab_url } " )
53
86
54
87
# Global configuration instance
@@ -156,25 +189,78 @@ def check_rate_limit() -> bool:
156
189
return True
157
190
158
191
def check_action_allowed (action : str ) -> bool :
159
- """Check if action is permitted"""
160
- if action in config .allowed_actions or "all" in config .allowed_actions :
192
+ """Check if action is permitted - hardcoded read-only mode for security"""
193
+ action_lower = action .lower ()
194
+
195
+ # Check against hardcoded allowed read-only actions
196
+ if action_lower in [act .lower () for act in config .allowed_actions ]:
161
197
return True
162
- logger .warning (f"🚫 Action not allowed: { action } " )
198
+
199
+ # Block any write actions with explicit security warning
200
+ forbidden_actions = ['write' , 'create' , 'update' , 'delete' , 'modify' , 'push' , 'merge' , 'approve' , 'deploy' , 'change' ]
201
+ if any (forbidden in action_lower for forbidden in forbidden_actions ):
202
+ logger .error (f"🚫🔒 SECURITY: Write action blocked: { action } " )
203
+ return False
204
+
205
+ # Log any unknown actions for security monitoring
206
+ logger .warning (f"🚫 Action not allowed: { action } (only read-only actions permitted)" )
163
207
return False
164
208
165
- def check_repo_allowed (project_id : str ) -> bool :
166
- """Check repository access permissions"""
167
- if not config .allowed_repos or config .allowed_repos == ['' ]:
209
+ def match_repo_pattern (project_path : str , pattern : str ) -> bool :
210
+ """
211
+ Check if a project path matches a given pattern.
212
+
213
+ Supports various patterns:
214
+ - Exact match: "my-repo" or "group/my-repo"
215
+ - Group wildcard: "group/*" (matches all repos in group)
216
+ - Subgroup wildcard: "group/subgroup/*" (matches all repos in subgroup)
217
+ - Mixed patterns: supports any combination
218
+
219
+ Args:
220
+ project_path: Full project path like "group/subgroup/project"
221
+ pattern: Pattern to match against, e.g. "group/*", "exact-name", etc.
222
+
223
+ Returns:
224
+ bool: True if project matches the pattern
225
+ """
226
+ if not pattern or not project_path :
227
+ return False
228
+
229
+ # Remove leading/trailing whitespace
230
+ pattern = pattern .strip ()
231
+ project_path = project_path .strip ()
232
+
233
+ # Exact match (including project ID as string)
234
+ if pattern == project_path :
168
235
return True
169
236
237
+ # Wildcard pattern matching
238
+ if '*' in pattern :
239
+ # Use fnmatch for shell-style wildcards
240
+ return fnmatch .fnmatch (project_path , pattern )
241
+
242
+ # No match
243
+ return False
244
+
245
+ def check_repo_allowed (project_id : str ) -> bool :
246
+ """Check repository access permissions against MCP_ALLOWED_REPOS patterns"""
170
247
try :
171
248
project = config .gl .projects .get (project_id )
172
249
project_name = project .path_with_namespace
173
250
174
- if project_id in config .allowed_repos or project_name in config .allowed_repos :
175
- return True
251
+ # Check against each allowed pattern
252
+ for allowed_pattern in config .allowed_repos :
253
+ allowed_pattern = allowed_pattern .strip ()
254
+
255
+ # Check if project ID matches (for backward compatibility)
256
+ if project_id == allowed_pattern :
257
+ return True
258
+
259
+ # Check if project path matches the pattern
260
+ if match_repo_pattern (project_name , allowed_pattern ):
261
+ return True
176
262
177
- logger .warning (f"🚫 Repo not allowed: { project_name } " )
263
+ logger .warning (f"🚫 Repo not allowed: { project_name } (allowed patterns: { config . allowed_repos } ) " )
178
264
return False
179
265
except Exception :
180
266
logger .warning (f"🚫 Could not verify repo access: { project_id } " )
@@ -741,4 +827,4 @@ def main():
741
827
mcp .run ()
742
828
743
829
if __name__ == "__main__" :
744
- main ()
830
+ main ()
0 commit comments