From 6bbb4f3e1926766d1bed5fba4e2c7eeb01395ff5 Mon Sep 17 00:00:00 2001 From: Alona Date: Tue, 30 Jun 2026 11:56:55 -0500 Subject: [PATCH] fix(jira-dc): don't org-gate a personal-workspace Jira DC integration (#15036) --- .../jira_dc_conversation_secret_enricher.py | 6 + ...st_jira_dc_conversation_secret_enricher.py | 113 +++++++++++++++++- 2 files changed, 118 insertions(+), 1 deletion(-) diff --git a/enterprise/integrations/jira_dc/jira_dc_conversation_secret_enricher.py b/enterprise/integrations/jira_dc/jira_dc_conversation_secret_enricher.py index bb453bba79..7349ad2ee7 100644 --- a/enterprise/integrations/jira_dc/jira_dc_conversation_secret_enricher.py +++ b/enterprise/integrations/jira_dc/jira_dc_conversation_secret_enricher.py @@ -90,6 +90,12 @@ async def _workspace_matches_context( workspace_org_id = getattr(workspace, 'org_id', None) if workspace_org_id is None: return True + # A workspace stamped with its creator's *personal* org (org_id == the + # admin's own user id) is an instance-wide integration, not a tenant + # boundary, so don't org-gate it. Real team/default orgs keep enforcing. + admin_user_id = getattr(workspace, 'admin_user_id', None) + if admin_user_id is not None and str(workspace_org_id) == str(admin_user_id): + return True return await _effective_org_matches( workspace_id=workspace.id, workspace_org_id=workspace_org_id, diff --git a/enterprise/tests/unit/integrations/jira_dc/test_jira_dc_conversation_secret_enricher.py b/enterprise/tests/unit/integrations/jira_dc/test_jira_dc_conversation_secret_enricher.py index 1606d9d967..82f5927eea 100644 --- a/enterprise/tests/unit/integrations/jira_dc/test_jira_dc_conversation_secret_enricher.py +++ b/enterprise/tests/unit/integrations/jira_dc/test_jira_dc_conversation_secret_enricher.py @@ -32,7 +32,7 @@ class FakeUserContext: return self.user_id -def _linked_store(*, org_id: UUID | None = ORG_ID): +def _linked_store(*, org_id: UUID | None = ORG_ID, admin_user_id: str = 'admin-user'): store = MagicMock() store.get_user_by_active_workspace = AsyncMock( return_value=SimpleNamespace(jira_dc_workspace_id=7) @@ -43,6 +43,7 @@ def _linked_store(*, org_id: UUID | None = ORG_ID): name='jira.example.com', status='active', org_id=org_id, + admin_user_id=admin_user_id, ) ) return store @@ -197,3 +198,113 @@ async def test_enricher_propagates_token_error_for_jira_triggered_start(): jwt_service=MagicMock(), access_token_hard_timeout=timedelta(minutes=5), ) + + +# A personal org's id == its owner's user id, so a JDC workspace created in +# personal-workspace mode is stamped org_id == admin_user_id. +PERSONAL_ORG_ID = UUID('00000000-0000-0000-0000-000000000789') + + +@pytest.mark.asyncio +async def test_enricher_allows_personal_workspace_org_for_non_creator(): + # Personal-workspace install: the JDC workspace (one company's Jira) is + # stamped with its creator's personal org. A *different* user must still get + # their own token -- it's an instance-wide integration, not a tenant boundary. + store = _linked_store(org_id=PERSONAL_ORG_ID, admin_user_id=str(PERSONAL_ORG_ID)) + jwt_service = MagicMock() + jwt_service.create_jws_token.return_value = 'signed-token' + + with ( + patch( + 'integrations.jira_dc.jira_dc_conversation_secret_enricher.JIRA_DC_ENABLE_OAUTH', + True, + ), + patch( + 'integrations.jira_dc.jira_dc_conversation_secret_enricher.JIRA_DC_BASE_URL', + 'https://jira.example.com', + ), + patch( + 'integrations.jira_dc.jira_dc_conversation_secret_enricher.JiraDcIntegrationStore.get_instance', + return_value=store, + ), + ): + enrichment = await JiraDcConversationSecretEnricher().enrich( + user_context=FakeUserContext(user_id='other-user', org_id=ORG_ID), + user=MagicMock(id='other-user'), + trigger=ConversationTrigger.SLACK, + system_message_suffix=None, + web_url='https://openhands.example.com', + jwt_service=jwt_service, + access_token_hard_timeout=timedelta(minutes=5), + ) + + assert 'JIRA_DC_TOKEN' in enrichment.secrets + assert 'JIRA_DC_TOKEN' in (enrichment.system_message_suffix or '') + + +@pytest.mark.asyncio +async def test_enricher_still_blocks_real_cross_org_workspace(): + # A genuine team/default org (org_id != admin_user_id) keeps enforcing + # isolation: a user from a different org gets no token. + store = _linked_store(org_id=OTHER_ORG_ID, admin_user_id='admin-user') + + with ( + patch( + 'integrations.jira_dc.jira_dc_conversation_secret_enricher.JIRA_DC_ENABLE_OAUTH', + True, + ), + patch( + 'integrations.jira_dc.jira_dc_conversation_secret_enricher.JIRA_DC_BASE_URL', + 'https://jira.example.com', + ), + patch( + 'integrations.jira_dc.jira_dc_conversation_secret_enricher.JiraDcIntegrationStore.get_instance', + return_value=store, + ), + ): + enrichment = await JiraDcConversationSecretEnricher().enrich( + user_context=FakeUserContext(org_id=ORG_ID), + user=MagicMock(id='kc-user'), + trigger=ConversationTrigger.SLACK, + system_message_suffix='Existing instructions.', + web_url='https://openhands.example.com', + jwt_service=MagicMock(), + access_token_hard_timeout=timedelta(minutes=5), + ) + + assert enrichment.secrets == {} + assert enrichment.system_message_suffix == 'Existing instructions.' + + +@pytest.mark.asyncio +async def test_enricher_injects_for_null_org_workspace(): + # An unscoped workspace (org_id is None) is instance-wide -- regression guard. + store = _linked_store(org_id=None) + jwt_service = MagicMock() + jwt_service.create_jws_token.return_value = 'signed-token' + + with ( + patch( + 'integrations.jira_dc.jira_dc_conversation_secret_enricher.JIRA_DC_ENABLE_OAUTH', + True, + ), + patch( + 'integrations.jira_dc.jira_dc_conversation_secret_enricher.JIRA_DC_BASE_URL', + 'https://jira.example.com', + ), + patch( + 'integrations.jira_dc.jira_dc_conversation_secret_enricher.JiraDcIntegrationStore.get_instance', + return_value=store, + ), + ): + enrichment = await JiraDcConversationSecretEnricher().enrich( + user_context=FakeUserContext(user_id='anyone', org_id=None), + user=MagicMock(id='anyone'), + trigger=ConversationTrigger.SLACK, + system_message_suffix=None, + web_url='https://openhands.example.com', + jwt_service=jwt_service, + access_token_hard_timeout=timedelta(minutes=5), + ) + + assert 'JIRA_DC_TOKEN' in enrichment.secrets