Skip to content

Commit 42a79c9

Browse files
Marek DanoMarek Dano
authored andcommitted
fix: playwright admin_api fixture to stop duplicating JWT auth, fix linting
Signed-off-by: Marek Dano <Marek.Dano@ibm.com>
1 parent d785882 commit 42a79c9

File tree

10 files changed

+77
-103
lines changed

10 files changed

+77
-103
lines changed

mcpgateway/alembic/versions/d3e4f5a6b7c8_add_binding_reference_id_to_tool_plugin_bindings.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
from typing import Sequence, Union
1111

1212
# Third-Party
13-
import sqlalchemy as sa
1413
from alembic import op
14+
import sqlalchemy as sa
1515

1616
# revision identifiers, used by Alembic.
1717
revision: str = "d3e4f5a6b7c8" # pragma: allowlist secret

mcpgateway/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ class Settings(BaseSettings):
310310
basic_auth_user: str = "admin"
311311
basic_auth_password: SecretStr = Field(default=SecretStr("changeme"))
312312
jwt_algorithm: str = "HS256"
313-
jwt_secret_key: SecretStr = Field(default=SecretStr("my-test-key"))
313+
jwt_secret_key: SecretStr = Field(default=SecretStr("my-test-key-but-now-longer-than-32-bytes"))
314314
jwt_public_key_path: str = ""
315315
jwt_private_key_path: str = ""
316316
jwt_audience: str = "mcpgateway-api"

mcpgateway/middleware/request_logging_middleware.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,8 @@
6262

6363
# First-Party
6464
from mcpgateway.auth import get_current_user
65-
from mcpgateway.config import settings
6665
from mcpgateway.common.validators import SecurityValidator
66+
from mcpgateway.config import settings
6767
from mcpgateway.middleware.path_filter import should_skip_request_logging
6868
from mcpgateway.services.logging_service import LoggingService
6969
from mcpgateway.services.structured_logger import get_structured_logger

mcpgateway/services/tool_plugin_binding_service.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -173,15 +173,9 @@ def upsert_bindings(
173173
# different external references are claiming the same (team, tool, plugin)
174174
# triple. The new reference_id wins (last-caller-wins), but the old
175175
# caller's DELETE by reference will now be a no-op.
176-
if (
177-
existing.binding_reference_id
178-
and policy.binding_reference_id
179-
and existing.binding_reference_id != policy.binding_reference_id
180-
):
176+
if existing.binding_reference_id and policy.binding_reference_id and existing.binding_reference_id != policy.binding_reference_id:
181177
logger.warning(
182-
"binding_reference_id ownership transfer: "
183-
"team=%s tool=%s plugin=%s old_ref=%s new_ref=%s — "
184-
"DELETE by old_ref will now be a no-op",
178+
"binding_reference_id ownership transfer: " "team=%s tool=%s plugin=%s old_ref=%s new_ref=%s — " "DELETE by old_ref will now be a no-op",
185179
team_id,
186180
tool_name,
187181
policy.plugin_id.value,
@@ -284,8 +278,7 @@ def list_bindings(
284278
if binding_reference_id:
285279
if team_id:
286280
logger.warning(
287-
"Both team_id=%r and binding_reference_id=%r supplied to list_bindings; "
288-
"team_id will be ignored. Omit team_id when filtering by binding_reference_id.",
281+
"Both team_id=%r and binding_reference_id=%r supplied to list_bindings; " "team_id will be ignored. Omit team_id when filtering by binding_reference_id.",
289282
team_id,
290283
binding_reference_id,
291284
)

tests/playwright/conftest.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,55 @@ def api_request_context(playwright: Playwright) -> Generator[APIRequestContext,
330330
request_context.dispose()
331331

332332

333+
@pytest.fixture(scope="session")
334+
def admin_api(playwright: Playwright) -> Generator[APIRequestContext, None, None]:
335+
"""Consolidated admin-authenticated API context for all Playwright tests.
336+
337+
Token resolution priority (fail-closed):
338+
1. MCP_AUTH env var (from Makefile / compose bootstrap) — preferred
339+
2. MCPGATEWAY_BEARER_TOKEN env var (legacy name used by other tools)
340+
3. Locally-signed JWT using Settings().jwt_secret_key (fallback)
341+
342+
This fixture replaces the four duplicate admin_api/owasp_admin_api fixtures
343+
previously scattered across entities, OWASP, operations, and teams conftests.
344+
Per-directory non-admin fixtures (viewer_api, owasp_user_a_api, etc.) remain
345+
local since they intentionally mint throwaway users.
346+
"""
347+
headers = {"Accept": "application/json"}
348+
349+
# Priority 1: MCP_AUTH (Makefile-generated token signed with gateway's secret)
350+
token = os.getenv("MCP_AUTH", "")
351+
if not token:
352+
# Priority 2: MCPGATEWAY_BEARER_TOKEN (legacy name)
353+
token = os.getenv("MCPGATEWAY_BEARER_TOKEN", "")
354+
if not token and not DISABLE_JWT_FALLBACK:
355+
# Priority 3: Locally-signed JWT fallback
356+
try:
357+
token = _create_jwt_token(
358+
{"sub": ADMIN_EMAIL},
359+
user_data={
360+
"email": ADMIN_EMAIL,
361+
"full_name": "Test Admin",
362+
"is_admin": True,
363+
"auth_provider": "test",
364+
},
365+
teams=None, # Admin bypass: null teams with is_admin=true
366+
)
367+
except Exception:
368+
pass # Use empty if generation fails
369+
370+
auth_header = _format_auth_header(token)
371+
if auth_header:
372+
headers["Authorization"] = auth_header
373+
374+
request_context = playwright.request.new_context(
375+
base_url=BASE_URL,
376+
extra_http_headers=headers,
377+
)
378+
yield request_context
379+
request_context.dispose()
380+
381+
333382
@pytest.fixture(scope="session")
334383
def browser_context_args(
335384
pytestconfig,

tests/playwright/entities/test_entity_lifecycle.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,23 +36,6 @@ def _make_jwt(email: str, is_admin: bool = False, teams=None) -> str:
3636
)
3737

3838

39-
@pytest.fixture(scope="module")
40-
def admin_api(playwright: Playwright):
41-
"""Admin-authenticated API context.
42-
43-
Prefers the ``MCP_AUTH`` env var (set by the Makefile from a token signed with
44-
the running gateway's secret) so signatures match the deployed instance. Falls
45-
back to a locally-signed JWT only when ``MCP_AUTH`` is unset.
46-
"""
47-
token = os.getenv("MCP_AUTH", "") or _make_jwt("admin@example.com", is_admin=True)
48-
ctx = playwright.request.new_context(
49-
base_url=BASE_URL,
50-
extra_http_headers={"Authorization": f"Bearer {token}", "Accept": "application/json"},
51-
)
52-
yield ctx
53-
ctx.dispose()
54-
55-
5639
@pytest.fixture(scope="module")
5740
def viewer_api(playwright: Playwright):
5841
"""Viewer (non-admin, no RBAC roles) API context for permission checks."""

tests/playwright/operations/conftest.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,6 @@ def _make_jwt(email: str, is_admin: bool = False, teams=None) -> str:
2929
)
3030

3131

32-
@pytest.fixture(scope="module")
33-
def admin_api(playwright: Playwright) -> Generator[APIRequestContext, None, None]:
34-
"""Admin-authenticated API context.
35-
36-
Prefers the ``MCP_AUTH`` env var (set by the Makefile from a token signed with
37-
the running gateway's secret) so signatures match the deployed instance. Falls
38-
back to a locally-signed JWT only when ``MCP_AUTH`` is unset.
39-
"""
40-
token = os.getenv("MCP_AUTH", "") or _make_jwt("admin@example.com", is_admin=True)
41-
ctx = playwright.request.new_context(
42-
base_url=BASE_URL,
43-
extra_http_headers={"Authorization": f"Bearer {token}", "Accept": "application/json"},
44-
)
45-
yield ctx
46-
ctx.dispose()
47-
48-
4932
@pytest.fixture(scope="module")
5033
def non_admin_api(playwright: Playwright) -> Generator[APIRequestContext, None, None]:
5134
"""Non-admin API context for permission checks."""

tests/playwright/security/owasp/conftest.py

Lines changed: 18 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -53,28 +53,11 @@ def owasp_anon_api(playwright: Playwright) -> Generator[APIRequestContext, None,
5353
ctx.dispose()
5454

5555

56-
@pytest.fixture(scope="module")
57-
def owasp_admin_api(playwright: Playwright) -> Generator[APIRequestContext, None, None]:
58-
"""Admin-authenticated API context for OWASP A01 tests (admin bypass via teams=null).
59-
60-
Prefers the ``MCP_AUTH`` env var (set by the Makefile from a token signed with
61-
the running gateway's secret) so signatures match the deployed instance. Falls
62-
back to a locally-signed JWT only when ``MCP_AUTH`` is unset.
63-
"""
64-
token = os.getenv("MCP_AUTH", "") or _make_jwt("admin@example.com", is_admin=True)
65-
ctx = playwright.request.new_context(
66-
base_url=BASE_URL,
67-
extra_http_headers={"Authorization": f"Bearer {token}", "Accept": "application/json"},
68-
)
69-
yield ctx
70-
ctx.dispose()
71-
72-
7356
@pytest.fixture
74-
def owasp_user_a_api(owasp_admin_api: APIRequestContext, playwright: Playwright):
57+
def owasp_user_a_api(admin_api: APIRequestContext, playwright: Playwright):
7558
"""Non-admin API context for User A, registered in the system. Cleans up after test."""
7659
email = f"owasp-a-{uuid.uuid4().hex[:8]}@example.com"
77-
create_resp = owasp_admin_api.post(
60+
create_resp = admin_api.post(
7861
"/auth/email/admin/users",
7962
data={"email": email, "password": TEST_PASSWORD, "full_name": "OWASP User A"},
8063
)
@@ -85,14 +68,14 @@ def owasp_user_a_api(owasp_admin_api: APIRequestContext, playwright: Playwright)
8568
yield {"ctx": ctx, "email": email}
8669
ctx.dispose()
8770
with suppress(Exception):
88-
owasp_admin_api.delete(f"/auth/email/admin/users/{email}")
71+
admin_api.delete(f"/auth/email/admin/users/{email}")
8972

9073

9174
@pytest.fixture
92-
def owasp_user_b_api(owasp_admin_api: APIRequestContext, playwright: Playwright):
75+
def owasp_user_b_api(admin_api: APIRequestContext, playwright: Playwright):
9376
"""Non-admin API context for User B, registered in the system. Cleans up after test."""
9477
email = f"owasp-b-{uuid.uuid4().hex[:8]}@example.com"
95-
create_resp = owasp_admin_api.post(
78+
create_resp = admin_api.post(
9679
"/auth/email/admin/users",
9780
data={"email": email, "password": TEST_PASSWORD, "full_name": "OWASP User B"},
9881
)
@@ -103,11 +86,11 @@ def owasp_user_b_api(owasp_admin_api: APIRequestContext, playwright: Playwright)
10386
yield {"ctx": ctx, "email": email}
10487
ctx.dispose()
10588
with suppress(Exception):
106-
owasp_admin_api.delete(f"/auth/email/admin/users/{email}")
89+
admin_api.delete(f"/auth/email/admin/users/{email}")
10790

10891

10992
@pytest.fixture
110-
def two_teams_setup(owasp_admin_api: APIRequestContext, playwright: Playwright):
93+
def two_teams_setup(admin_api: APIRequestContext, playwright: Playwright):
11194
"""Create two distinct teams with separate scoped tokens. Cleans up after test."""
11295
suffix = uuid.uuid4().hex[:8]
11396
team_a_id: str | None = None
@@ -118,25 +101,25 @@ def two_teams_setup(owasp_admin_api: APIRequestContext, playwright: Playwright):
118101
ctx_b = None
119102
try:
120103
# Team A
121-
resp_a = owasp_admin_api.post("/teams/", data={"name": f"owasp-team-a-{suffix}", "description": "OWASP Team A", "visibility": "private"})
104+
resp_a = admin_api.post("/teams/", data={"name": f"owasp-team-a-{suffix}", "description": "OWASP Team A", "visibility": "private"})
122105
assert resp_a.status in (200, 201), f"Failed creating Team A: {resp_a.status} {resp_a.text()}"
123106
team_a_id = resp_a.json()["id"]
124107

125108
# Team B
126-
resp_b = owasp_admin_api.post("/teams/", data={"name": f"owasp-team-b-{suffix}", "description": "OWASP Team B", "visibility": "private"})
109+
resp_b = admin_api.post("/teams/", data={"name": f"owasp-team-b-{suffix}", "description": "OWASP Team B", "visibility": "private"})
127110
assert resp_b.status in (200, 201), f"Failed creating Team B: {resp_b.status} {resp_b.text()}"
128111
team_b_id = resp_b.json()["id"]
129112

130113
# Server owned by Team A
131-
srv_a = owasp_admin_api.post(
114+
srv_a = admin_api.post(
132115
"/servers",
133116
data={"server": {"name": f"owasp-srv-a-{suffix}", "description": "Team A server"}, "team_id": team_a_id, "visibility": "team"},
134117
)
135118
assert srv_a.status in (200, 201), f"Failed creating Team A server: {srv_a.status} {srv_a.text()}"
136119
server_a_id = srv_a.json()["id"]
137120

138121
# Server owned by Team B
139-
srv_b = owasp_admin_api.post(
122+
srv_b = admin_api.post(
140123
"/servers",
141124
data={"server": {"name": f"owasp-srv-b-{suffix}", "description": "Team B server"}, "team_id": team_b_id, "visibility": "team"},
142125
)
@@ -163,24 +146,24 @@ def two_teams_setup(owasp_admin_api: APIRequestContext, playwright: Playwright):
163146
ctx_b.dispose()
164147
if server_a_id:
165148
with suppress(Exception):
166-
owasp_admin_api.delete(f"/servers/{server_a_id}")
149+
admin_api.delete(f"/servers/{server_a_id}")
167150
if server_b_id:
168151
with suppress(Exception):
169-
owasp_admin_api.delete(f"/servers/{server_b_id}")
152+
admin_api.delete(f"/servers/{server_b_id}")
170153
if team_a_id:
171154
with suppress(Exception):
172-
owasp_admin_api.delete(f"/teams/{team_a_id}")
155+
admin_api.delete(f"/teams/{team_a_id}")
173156
if team_b_id:
174157
with suppress(Exception):
175-
owasp_admin_api.delete(f"/teams/{team_b_id}")
158+
admin_api.delete(f"/teams/{team_b_id}")
176159

177160

178161
@pytest.fixture
179-
def private_server_owned_by_user_a(owasp_admin_api: APIRequestContext, owasp_user_a_api: dict):
162+
def private_server_owned_by_user_a(admin_api: APIRequestContext, owasp_user_a_api: dict):
180163
"""Create a private server via admin on behalf of User A and return its ID. Cleans up after test."""
181164
server_id: str | None = None
182165
try:
183-
resp = owasp_admin_api.post(
166+
resp = admin_api.post(
184167
"/servers",
185168
data={
186169
"server": {"name": f"owasp-priv-{uuid.uuid4().hex[:8]}", "description": "User A private server"},
@@ -195,4 +178,4 @@ def private_server_owned_by_user_a(owasp_admin_api: APIRequestContext, owasp_use
195178
finally:
196179
if server_id:
197180
with suppress(Exception):
198-
owasp_admin_api.delete(f"/servers/{server_id}")
181+
admin_api.delete(f"/servers/{server_id}")

tests/playwright/security/owasp/test_a01_broken_access_control.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,10 +213,10 @@ class TestVerticalPrivilegeEscalation:
213213
"""CWE-269, CWE-285 – Non-admin authenticated users cannot call admin-only endpoints."""
214214

215215
@pytest.fixture(scope="class")
216-
def non_admin_ctx(self, owasp_admin_api: APIRequestContext, playwright: Playwright) -> APIRequestContext:
217-
"""Register a non-admin user via owasp_admin_api, then yield an API context for them."""
216+
def non_admin_ctx(self, admin_api: APIRequestContext, playwright: Playwright) -> APIRequestContext:
217+
"""Register a non-admin user via admin_api, then yield an API context for them."""
218218
email = f"nonadmin-a01-{uuid.uuid4().hex[:8]}@example.com"
219-
create_resp = owasp_admin_api.post(
219+
create_resp = admin_api.post(
220220
"/auth/email/admin/users",
221221
data={"email": email, "password": TEST_PASSWORD, "full_name": "Non-Admin A01"},
222222
)
@@ -226,7 +226,7 @@ def non_admin_ctx(self, owasp_admin_api: APIRequestContext, playwright: Playwrig
226226
yield ctx
227227
ctx.dispose()
228228
with suppress(Exception):
229-
owasp_admin_api.delete(f"/auth/email/admin/users/{email}")
229+
admin_api.delete(f"/auth/email/admin/users/{email}")
230230

231231
def test_non_admin_cannot_list_all_users(self, non_admin_ctx: APIRequestContext) -> None:
232232
resp = non_admin_ctx.get("/auth/email/admin/users")

tests/playwright/teams/conftest.py

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -71,23 +71,6 @@ def invite_and_accept(admin_api: APIRequestContext, playwright: Playwright, team
7171
return inv_data
7272

7373

74-
@pytest.fixture(scope="module")
75-
def admin_api(playwright: Playwright) -> Generator[APIRequestContext, None, None]:
76-
"""Admin-authenticated API context for team tests.
77-
78-
Prefers the ``MCP_AUTH`` env var (set by the Makefile from a token signed with
79-
the running gateway's secret) so signatures match the deployed instance. Falls
80-
back to a locally-signed JWT only when ``MCP_AUTH`` is unset.
81-
"""
82-
token = os.getenv("MCP_AUTH", "") or _make_jwt("admin@example.com", is_admin=True)
83-
ctx = playwright.request.new_context(
84-
base_url=BASE_URL,
85-
extra_http_headers={"Authorization": f"Bearer {token}", "Accept": "application/json"},
86-
)
87-
yield ctx
88-
ctx.dispose()
89-
90-
9174
@pytest.fixture(scope="module")
9275
def private_team(admin_api: APIRequestContext):
9376
"""Create a private team for invitation tests, cleanup after module."""

0 commit comments

Comments
 (0)