Initial curated ITWorx_MCP_Hub source from 368fba691ea8d23403b4d56b53b5ec670d7c5020
This commit is contained in:
commit
cb5be898ea
663 files changed
+82674
No files matched your search
@@ -0,0 +1,266 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import unittest
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from uuid import uuid4
|
||||
|
||||
from services.control_api.auth.config import OIDCSettings
|
||||
from services.control_api.auth.errors import InvalidToken
|
||||
from services.control_api.auth.fake_provider import FakeOIDCProvider
|
||||
from services.control_api.auth.provider import OIDCProviderClient
|
||||
from services.runtime.ingestion_auth import (
|
||||
ActorContext,
|
||||
authorize_actor,
|
||||
authorize_workspace_actor,
|
||||
build_ingestion_oidc_provider,
|
||||
create_actor_context,
|
||||
sanitize_ingestion_audit,
|
||||
)
|
||||
|
||||
|
||||
class IngestionAuthTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.fake = FakeOIDCProvider(
|
||||
issuer="https://auth.example.test/application/o/ingestion/",
|
||||
audience="knowledge-ingestion",
|
||||
)
|
||||
self.settings = OIDCSettings(
|
||||
issuer=self.fake.issuer,
|
||||
audience=self.fake.audience,
|
||||
client_id="knowledge-ingestion",
|
||||
redirect_uri="https://mcp.example.test/callback",
|
||||
)
|
||||
self.provider = OIDCProviderClient(self.settings, transport=self.fake.transport())
|
||||
self.project_id = str(uuid4())
|
||||
self.now = datetime.now(UTC)
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.provider.close()
|
||||
|
||||
def token(self, **overrides: object) -> str:
|
||||
claims: dict[str, object] = {
|
||||
"sub": "external-actor-42",
|
||||
"iss": self.fake.issuer,
|
||||
"aud": self.fake.audience,
|
||||
"iat": int(self.now.timestamp()),
|
||||
"exp": int((self.now + timedelta(minutes=5)).timestamp()),
|
||||
"projects": [self.project_id],
|
||||
"scope": "knowledge:read ingestion:write",
|
||||
}
|
||||
claims.update(overrides)
|
||||
return self.fake._encode(claims)
|
||||
|
||||
def test_actor_context_comes_from_a_signature_issuer_and_audience_validated_token(self) -> None:
|
||||
actor = asyncio.run(
|
||||
create_actor_context(
|
||||
f"Bearer {self.token()}",
|
||||
provider=self.provider,
|
||||
correlation_id="trace-2026-01",
|
||||
now=self.now,
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(actor.subject, "external-actor-42")
|
||||
self.assertEqual(actor.projects, frozenset({self.project_id}))
|
||||
self.assertEqual(actor.scopes, frozenset({"knowledge:read", "ingestion:write"}))
|
||||
self.assertEqual(actor.correlation_id, "trace-2026-01")
|
||||
self.assertNotIn("Bearer", repr(actor))
|
||||
|
||||
def test_invalid_signature_issuer_and_audience_are_rejected(self) -> None:
|
||||
valid = self.token()
|
||||
with self.assertRaises(InvalidToken):
|
||||
asyncio.run(
|
||||
create_actor_context(
|
||||
f"Bearer {valid[:-3]}bad", provider=self.provider, correlation_id="trace-1"
|
||||
)
|
||||
)
|
||||
with self.assertRaises(InvalidToken):
|
||||
asyncio.run(
|
||||
create_actor_context(
|
||||
f"Bearer {self.token(iss='https://foreign.example.test/')}",
|
||||
provider=self.provider,
|
||||
correlation_id="trace-1",
|
||||
)
|
||||
)
|
||||
with self.assertRaises(InvalidToken):
|
||||
asyncio.run(
|
||||
create_actor_context(
|
||||
f"Bearer {self.token(aud='another-service')}",
|
||||
provider=self.provider,
|
||||
correlation_id="trace-1",
|
||||
)
|
||||
)
|
||||
|
||||
def test_missing_or_malformed_project_and_scope_claims_fail_closed(self) -> None:
|
||||
for claims in (
|
||||
{"projects": []},
|
||||
{"projects": "not-an-array"},
|
||||
{"scope": ["ingestion:write"]},
|
||||
{"projects": ["../other-project"]},
|
||||
{"scope": "ingestion:write unexpected scope\nforged"},
|
||||
):
|
||||
with self.subTest(claims=claims), self.assertRaises(InvalidToken):
|
||||
asyncio.run(
|
||||
create_actor_context(
|
||||
f"Bearer {self.token(**claims)}",
|
||||
provider=self.provider,
|
||||
correlation_id="trace-1",
|
||||
)
|
||||
)
|
||||
|
||||
def test_authorization_requires_the_requested_scope_and_project(self) -> None:
|
||||
actor = ActorContext(
|
||||
subject="external-actor-42",
|
||||
projects=frozenset({self.project_id}),
|
||||
scopes=frozenset({"knowledge:read"}),
|
||||
correlation_id="trace-1",
|
||||
)
|
||||
|
||||
authorize_actor(actor, capability="knowledge:read", project=self.project_id)
|
||||
with self.assertRaises(PermissionError):
|
||||
authorize_actor(actor, capability="ingestion:write", project=self.project_id)
|
||||
with self.assertRaises(PermissionError):
|
||||
authorize_actor(actor, capability="knowledge:read", project=str(uuid4()))
|
||||
|
||||
def test_workspace_identity_is_optional_and_preserved_only_as_explicit_trusted_context(
|
||||
self,
|
||||
) -> None:
|
||||
ordinary = ActorContext(
|
||||
subject="external-actor-42",
|
||||
projects=frozenset({self.project_id}),
|
||||
scopes=frozenset({"knowledge:read"}),
|
||||
correlation_id="trace-1",
|
||||
)
|
||||
trusted = ActorContext(
|
||||
subject="external-actor-42",
|
||||
projects=frozenset({self.project_id}),
|
||||
scopes=frozenset({"knowledge:read"}),
|
||||
correlation_id="trace-1",
|
||||
connector_project="knowledge-hub",
|
||||
connector_environment="prod",
|
||||
)
|
||||
|
||||
self.assertIsNone(ordinary.connector_project)
|
||||
self.assertIsNone(ordinary.connector_environment)
|
||||
self.assertEqual(trusted.connector_project, "knowledge-hub")
|
||||
self.assertEqual(trusted.connector_environment, "prod")
|
||||
|
||||
def test_workspace_actor_requires_read_scope_and_trusted_connector_environment(self) -> None:
|
||||
trusted = ActorContext(
|
||||
subject="external-actor-42",
|
||||
projects=frozenset({self.project_id}),
|
||||
scopes=frozenset({"knowledge:read"}),
|
||||
correlation_id="trace-1",
|
||||
connector_project="knowledge-hub",
|
||||
connector_environment="prod",
|
||||
)
|
||||
authorize_workspace_actor(trusted)
|
||||
|
||||
untrusted_actors = (
|
||||
ActorContext(
|
||||
subject="external-actor-42",
|
||||
projects=frozenset({self.project_id}),
|
||||
scopes=frozenset({"knowledge:read"}),
|
||||
correlation_id="trace-1",
|
||||
),
|
||||
ActorContext(
|
||||
subject="external-actor-42",
|
||||
projects=frozenset({self.project_id}),
|
||||
scopes=frozenset({"knowledge:read"}),
|
||||
correlation_id="trace-1",
|
||||
connector_project="another-project",
|
||||
connector_environment="prod",
|
||||
),
|
||||
ActorContext(
|
||||
subject="external-actor-42",
|
||||
projects=frozenset({self.project_id}),
|
||||
scopes=frozenset({"knowledge:read"}),
|
||||
correlation_id="trace-1",
|
||||
connector_project="knowledge-hub",
|
||||
connector_environment="dev",
|
||||
),
|
||||
ActorContext(
|
||||
subject="external-actor-42",
|
||||
projects=frozenset({self.project_id}),
|
||||
scopes=frozenset({"ingestion:write"}),
|
||||
correlation_id="trace-1",
|
||||
connector_project="knowledge-hub",
|
||||
connector_environment="prod",
|
||||
),
|
||||
)
|
||||
for actor in untrusted_actors:
|
||||
with self.subTest(actor=actor), self.assertRaises(PermissionError):
|
||||
authorize_workspace_actor(actor)
|
||||
|
||||
with self.assertRaises(PermissionError):
|
||||
authorize_workspace_actor(None) # type: ignore[arg-type]
|
||||
|
||||
def test_additional_provider_settings_are_complete_and_exact(self) -> None:
|
||||
env = {
|
||||
"HUB_MCP_ADDITIONAL_OIDC_ISSUER": self.fake.issuer,
|
||||
"HUB_MCP_ADDITIONAL_OIDC_AUDIENCE": self.fake.audience,
|
||||
"HUB_MCP_ADDITIONAL_OIDC_CLIENT_ID": "knowledge-ingestion",
|
||||
"HUB_MCP_ADDITIONAL_OIDC_REDIRECT_URI": "https://mcp.example.test/callback",
|
||||
}
|
||||
provider = build_ingestion_oidc_provider(environ=env, transport=self.fake.transport())
|
||||
try:
|
||||
self.assertEqual(str(provider.settings.issuer), self.fake.issuer)
|
||||
self.assertEqual(provider.settings.audience, self.fake.audience)
|
||||
finally:
|
||||
provider.close()
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
build_ingestion_oidc_provider(
|
||||
environ={"HUB_MCP_ADDITIONAL_OIDC_ISSUER": self.fake.issuer}
|
||||
)
|
||||
|
||||
def test_ingestion_audit_accepts_only_known_file_error_codes(self) -> None:
|
||||
audit = sanitize_ingestion_audit(
|
||||
{
|
||||
"tool": "extract_knowledge_files",
|
||||
"result": "failure",
|
||||
"status": 503,
|
||||
"latency_bucket": "lt_100ms",
|
||||
"correlation_id": "trace-safe",
|
||||
"error_code": "invalid_input",
|
||||
"input_reason": "attachment_file_id_invalid",
|
||||
"file_id": "private-file-reference",
|
||||
"download_url": "private-download-reference",
|
||||
"content": "private source content",
|
||||
}
|
||||
)
|
||||
self.assertEqual(
|
||||
audit,
|
||||
{
|
||||
"tool": "extract_knowledge_files",
|
||||
"result": "failure",
|
||||
"status": 503,
|
||||
"latency_bucket": "lt_100ms",
|
||||
"correlation_id": "trace-safe",
|
||||
"error_code": "invalid_input",
|
||||
"input_reason": "attachment_file_id_invalid",
|
||||
},
|
||||
)
|
||||
|
||||
for reason in (
|
||||
"attachment_file_id_empty",
|
||||
"attachment_file_id_too_long",
|
||||
"attachment_file_id_non_printable",
|
||||
):
|
||||
self.assertEqual(
|
||||
sanitize_ingestion_audit(
|
||||
{"tool": "extract_knowledge_files", "input_reason": reason}
|
||||
)["input_reason"],
|
||||
reason,
|
||||
)
|
||||
|
||||
unrecognized = sanitize_ingestion_audit(
|
||||
{"tool": "extract_knowledge_files", "error_code": "private-file-reference"}
|
||||
)
|
||||
self.assertNotIn("error_code", unrecognized)
|
||||
|
||||
private_reason = sanitize_ingestion_audit(
|
||||
{"tool": "extract_knowledge_files", "input_reason": "private-file-reference"}
|
||||
)
|
||||
self.assertNotIn("input_reason", private_reason)
|
||||
Reference in new issue
Block a user