Skip to content

Commit cd37035

Browse files
committed
fix: #1370 Custom Headers now are visible when editing saved servers
Signed-off-by: Aakash <[email protected]>
1 parent bc2f512 commit cd37035

File tree

5 files changed

+350
-34
lines changed

5 files changed

+350
-34
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,9 @@ This release delivers **REST API Passthrough Capabilities**, **API & UI Paginati
234234
* **Non-root Container Users** (#1231) - Added non-root user to scratch Go containers
235235
* **Container Runtime Detection** - Improved Docker/Podman detection in Makefile
236236

237+
#### **💻 Admin UI Fixes** (#1370)
238+
* **Saved custom headers not visible** (#1370) - Fixed custom headers not visible to Admins when editing a MCP server using custom headers for auth.
239+
237240
### Changed
238241

239242
#### **🗄️ Database Schema & Multi-Tenancy Enhancements** (#1246, #1273)

mcpgateway/schemas.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3011,6 +3011,8 @@ class GatewayRead(BaseModelWithConfigDict):
30113011
# Authorizations
30123012
auth_type: Optional[str] = Field(None, description="auth_type: basic, bearer, headers, oauth, or None")
30133013
auth_value: Optional[str] = Field(None, description="auth value: username/password or token or custom headers")
3014+
auth_headers: Optional[List[Dict[str, str]]] = Field(default=None, description="List of custom headers for authentication")
3015+
auth_headers_unmasked: Optional[List[Dict[str, str]]] = Field(default=None, description="Unmasked custom headers for administrative views")
30143016

30153017
# OAuth 2.0 configuration
30163018
oauth_config: Optional[Dict[str, Any]] = Field(None, description="OAuth 2.0 configuration including grant_type, client_id, encrypted client_secret, URLs, and scopes")
@@ -3023,6 +3025,10 @@ class GatewayRead(BaseModelWithConfigDict):
30233025
auth_header_value: Optional[str] = Field(None, description="vallue for custom headers authentication")
30243026
tags: List[str] = Field(default_factory=list, description="Tags for categorizing the gateway")
30253027

3028+
auth_password_unmasked: Optional[str] = Field(default=None, description="Unmasked password for basic authentication")
3029+
auth_token_unmasked: Optional[str] = Field(default=None, description="Unmasked bearer token for authentication")
3030+
auth_header_value_unmasked: Optional[str] = Field(default=None, description="Unmasked single custom header value")
3031+
30263032
# Team scoping fields for resource organization
30273033
team_id: Optional[str] = Field(None, description="Team ID this gateway belongs to")
30283034
team: Optional[str] = Field(None, description="Name of the team that owns this resource")
@@ -3135,19 +3141,30 @@ def _populate_auth(self) -> Self:
31353141
if not u or not p:
31363142
raise ValueError("basic auth requires both username and password")
31373143
self.auth_username, self.auth_password = u, p
3144+
self.auth_password_unmasked = p
31383145

31393146
elif auth_type == "bearer":
31403147
auth = auth_value.get("Authorization")
31413148
if not (isinstance(auth, str) and auth.startswith("Bearer ")):
31423149
raise ValueError("bearer auth requires an Authorization header of the form 'Bearer <token>'")
31433150
self.auth_token = auth.removeprefix("Bearer ")
3151+
self.auth_token_unmasked = self.auth_token
31443152

31453153
elif auth_type == "authheaders":
31463154
# For backward compatibility, populate first header in key/value fields
3147-
if len(auth_value) == 0:
3155+
if not isinstance(auth_value, dict) or len(auth_value) == 0:
31483156
raise ValueError("authheaders requires at least one key/value pair")
3157+
self.auth_headers = [
3158+
{"key": str(key), "value": "" if value is None else str(value)}
3159+
for key, value in auth_value.items()
3160+
]
3161+
self.auth_headers_unmasked = [
3162+
{"key": str(key), "value": "" if value is None else str(value)}
3163+
for key, value in auth_value.items()
3164+
]
31493165
k, v = next(iter(auth_value.items()))
31503166
self.auth_header_key, self.auth_header_value = k, v
3167+
self.auth_header_value_unmasked = v
31513168

31523169
return self
31533170

@@ -3182,7 +3199,23 @@ def masked(self) -> "GatewayRead":
31823199
masked_data["auth_password"] = settings.masked_auth_value if masked_data.get("auth_password") else None
31833200
masked_data["auth_token"] = settings.masked_auth_value if masked_data.get("auth_token") else None
31843201
masked_data["auth_header_value"] = settings.masked_auth_value if masked_data.get("auth_header_value") else None
3185-
3202+
if masked_data.get("auth_headers"):
3203+
masked_data["auth_headers"] = [
3204+
{
3205+
"key": header.get("key"),
3206+
"value": settings.masked_auth_value if header.get("value") else header.get("value"),
3207+
}
3208+
for header in masked_data["auth_headers"]
3209+
]
3210+
3211+
masked_data["auth_password_unmasked"] = self.auth_password_unmasked
3212+
masked_data["auth_token_unmasked"] = self.auth_token_unmasked
3213+
masked_data["auth_header_value_unmasked"] = self.auth_header_value_unmasked
3214+
masked_data["auth_headers_unmasked"] = (
3215+
[header.copy() for header in self.auth_headers_unmasked]
3216+
if self.auth_headers_unmasked
3217+
else None
3218+
)
31863219
return GatewayRead.model_validate(masked_data)
31873220

31883221

mcpgateway/services/gateway_service.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1192,7 +1192,27 @@ async def update_gateway(
11921192

11931193
# Support multiple custom headers on update
11941194
if hasattr(gateway_update, "auth_headers") and gateway_update.auth_headers:
1195-
header_dict = {h["key"]: h["value"] for h in gateway_update.auth_headers if h.get("key")}
1195+
existing_auth_raw = getattr(gateway, "auth_value", {}) or {}
1196+
if isinstance(existing_auth_raw, str):
1197+
try:
1198+
existing_auth = decode_auth(existing_auth_raw)
1199+
except Exception:
1200+
existing_auth = {}
1201+
elif isinstance(existing_auth_raw, dict):
1202+
existing_auth = existing_auth_raw
1203+
else:
1204+
existing_auth = {}
1205+
1206+
header_dict: Dict[str, str] = {}
1207+
for header in gateway_update.auth_headers:
1208+
key = header.get("key")
1209+
if not key:
1210+
continue
1211+
value = header.get("value", "")
1212+
if value == settings.masked_auth_value and key in existing_auth:
1213+
header_dict[key] = existing_auth[key]
1214+
else:
1215+
header_dict[key] = value
11961216
gateway.auth_value = header_dict # Store as dict for DB JSON field
11971217
elif settings.masked_auth_value not in (token, password, header_value):
11981218
# Check if values differ from existing ones or if setting for first time

0 commit comments

Comments
 (0)