Skip to content

Commit 08bc9c2

Browse files
authored
Merge pull request #1 from redhat-ai-tools/beingnishas-patch-1
No other operations are allowed, hard coded read For testing disabled ssl verification Allowed limiting repositories of interest, this will help in quicker responses
2 parents f41ebe8 + f6dd77b commit 08bc9c2

File tree

1 file changed

+101
-15
lines changed

1 file changed

+101
-15
lines changed

gitlab_mcp_server.py

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import json
99
import logging
1010
import time
11+
import fnmatch
1112
from collections import defaultdict
1213
from typing import Any, Dict, List, Optional
1314
import asyncio
@@ -28,8 +29,20 @@ class GitLabConfig:
2829
def __init__(self):
2930
# While using a client like cursor limit the api calls, actions & repos
3031
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.")
3346

3447
# Rate limiting tracking
3548
self.api_calls = defaultdict(list)
@@ -42,13 +55,33 @@ def __init__(self):
4255
if not self.gitlab_token:
4356
raise ValueError("GitLab token required. Set MCP_GITLAB_TOKEN environment variable")
4457

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+
4576
# Initialize GitLab client
46-
self.gl = gitlab.Gitlab(self.gitlab_url, private_token=self.gitlab_token)
77+
self.gl = gitlab.Gitlab(**gitlab_config)
4778

4879
# Log configuration
4980
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'")
5285
logger.info(f"🔗 GitLab URL: {self.gitlab_url}")
5386

5487
# Global configuration instance
@@ -156,25 +189,78 @@ def check_rate_limit() -> bool:
156189
return True
157190

158191
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]:
161197
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)")
163207
return False
164208

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:
168235
return True
169236

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"""
170247
try:
171248
project = config.gl.projects.get(project_id)
172249
project_name = project.path_with_namespace
173250

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
176262

177-
logger.warning(f"🚫 Repo not allowed: {project_name}")
263+
logger.warning(f"🚫 Repo not allowed: {project_name} (allowed patterns: {config.allowed_repos})")
178264
return False
179265
except Exception:
180266
logger.warning(f"🚫 Could not verify repo access: {project_id}")
@@ -741,4 +827,4 @@ def main():
741827
mcp.run()
742828

743829
if __name__ == "__main__":
744-
main()
830+
main()

0 commit comments

Comments
 (0)