From c9747aa8a84dae973d97e9f37eb6a7479750b3dc Mon Sep 17 00:00:00 2001 From: David Neale Date: Wed, 17 Sep 2025 12:32:36 +0100 Subject: [PATCH 1/2] fix: support BYOT OAuth mode without requiring service URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes: "com.atlassian.confluence.api.service.exceptions.GoneException: This deprecated endpoint has been removed." error when using ATLASSIAN_OAUTH_ENABLE (BYOT) + Atlassian Cloud. **Changes:** - Allow missing CONFLUENCE_URL/JIRA_URL when ATLASSIAN_OAUTH_ENABLE is set - Mark services as available in environment detection for BYOT mode - Preserve original is_cloud behavior based on oauth_config.cloud_id presence **Configuration behavior:** - ATLASSIAN_OAUTH_ENABLE=true + no URLs → creates minimal OAuth config - is_cloud determination depends on ATLASSIAN_OAUTH_CLOUD_ID presence: - With ATLASSIAN_OAUTH_CLOUD_ID → is_cloud = True (Cloud API) - Without ATLASSIAN_OAUTH_CLOUD_ID → is_cloud = False (Server API) - When URLs are provided, they override cloud detection as before **Files modified:** - src/mcp_atlassian/confluence/config.py - src/mcp_atlassian/jira/config.py - src/mcp_atlassian/utils/environment.py **Tests added:** - BYOT OAuth scenarios with/without URLs and cloud_id - Environment service detection for minimal OAuth mode - Various ATLASSIAN_OAUTH_ENABLE value format validation This enables BYOT (Bring Your Own Token) OAuth deployments where user tokens are provided via HTTP headers without requiring static service URL configuration, while maintaining backward compatibility. --- src/mcp_atlassian/confluence/config.py | 8 ++- src/mcp_atlassian/jira/config.py | 8 ++- src/mcp_atlassian/utils/environment.py | 6 ++- tests/unit/confluence/test_config.py | 64 +++++++++++++++++++++++ tests/unit/jira/test_config.py | 64 +++++++++++++++++++++++ tests/unit/utils/test_environment.py | 71 ++++++++++++++++++++++++++ 6 files changed, 215 insertions(+), 6 deletions(-) diff --git a/src/mcp_atlassian/confluence/config.py b/src/mcp_atlassian/confluence/config.py index 37d5f7c4b..fac27323f 100644 --- a/src/mcp_atlassian/confluence/config.py +++ b/src/mcp_atlassian/confluence/config.py @@ -90,8 +90,12 @@ def from_env(cls) -> "ConfluenceConfig": oauth_config = get_oauth_config_from_env() auth_type = None - # Use the shared utility function directly - is_cloud = is_atlassian_cloud_url(url) + if url: + is_cloud = is_atlassian_cloud_url(url) + else: + # When no URL, let the @property is_cloud method handle detection + # It will check for OAuth cloud_id or return False + is_cloud = False if oauth_config: # OAuth is available - could be full config or minimal config for user-provided tokens diff --git a/src/mcp_atlassian/jira/config.py b/src/mcp_atlassian/jira/config.py index 323e89a07..415be0118 100644 --- a/src/mcp_atlassian/jira/config.py +++ b/src/mcp_atlassian/jira/config.py @@ -90,8 +90,12 @@ def from_env(cls) -> "JiraConfig": oauth_config = get_oauth_config_from_env() auth_type = None - # Use the shared utility function directly - is_cloud = is_atlassian_cloud_url(url) + if url: + is_cloud = is_atlassian_cloud_url(url) + else: + # When no URL, let the @property is_cloud method handle detection + # It will check for OAuth cloud_id or return False + is_cloud = False if oauth_config: # OAuth is available - could be full config or minimal config for user-provided tokens diff --git a/src/mcp_atlassian/utils/environment.py b/src/mcp_atlassian/utils/environment.py index 0a1799a62..a4c3b2b93 100644 --- a/src/mcp_atlassian/utils/environment.py +++ b/src/mcp_atlassian/utils/environment.py @@ -59,7 +59,8 @@ def get_available_services() -> dict[str, bool | None]: logger.info( "Using Confluence Server/Data Center authentication (PAT or Basic Auth)" ) - elif os.getenv("ATLASSIAN_OAUTH_ENABLE", "").lower() in ("true", "1", "yes"): + + if os.getenv("ATLASSIAN_OAUTH_ENABLE", "").lower() in ("true", "1", "yes"): confluence_is_setup = True logger.info( "Using Confluence minimal OAuth configuration - expecting user-provided tokens via headers" @@ -112,7 +113,8 @@ def get_available_services() -> dict[str, bool | None]: logger.info( "Using Jira Server/Data Center authentication (PAT or Basic Auth)" ) - elif os.getenv("ATLASSIAN_OAUTH_ENABLE", "").lower() in ("true", "1", "yes"): + + if os.getenv("ATLASSIAN_OAUTH_ENABLE", "").lower() in ("true", "1", "yes"): jira_is_setup = True logger.info( "Using Jira minimal OAuth configuration - expecting user-provided tokens via headers" diff --git a/tests/unit/confluence/test_config.py b/tests/unit/confluence/test_config.py index 6e7fd3010..3dd2964db 100644 --- a/tests/unit/confluence/test_config.py +++ b/tests/unit/confluence/test_config.py @@ -183,3 +183,67 @@ def test_is_cloud_oauth_with_cloud_id(): oauth_config=oauth_config, ) assert config.is_cloud is True + + +def test_from_env_oauth_enable_no_url(): + """Test BYOT OAuth mode - ATLASSIAN_OAUTH_ENABLE=true without URL or cloud_id.""" + with patch.dict( + os.environ, + { + "ATLASSIAN_OAUTH_ENABLE": "true", + # No CONFLUENCE_URL set + # No ATLASSIAN_OAUTH_CLOUD_ID set + }, + clear=True, + ): + config = ConfluenceConfig.from_env() + assert config.auth_type == "oauth" + assert config.is_cloud is False + + +def test_from_env_oauth_enable_no_url_with_cloud_id(): + """Test BYOT OAuth mode - ATLASSIAN_OAUTH_ENABLE=true without URL but with cloud_id.""" + with patch.dict( + os.environ, + { + "ATLASSIAN_OAUTH_ENABLE": "true", + "ATLASSIAN_OAUTH_CLOUD_ID": "test-cloud-id", + # No CONFLUENCE_URL set + }, + clear=True, + ): + config = ConfluenceConfig.from_env() + assert config.auth_type == "oauth" + assert config.is_cloud is True + + +def test_from_env_oauth_enable_with_cloud_url(): + """Test BYOT OAuth mode - ATLASSIAN_OAUTH_ENABLE=true with Cloud URL.""" + with patch.dict( + os.environ, + { + "ATLASSIAN_OAUTH_ENABLE": "true", + "CONFLUENCE_URL": "https://test.atlassian.net/wiki", + }, + clear=True, + ): + config = ConfluenceConfig.from_env() + assert config.url == "https://test.atlassian.net/wiki" + assert config.auth_type == "oauth" + assert config.is_cloud is True # Should be Cloud based on URL + + +def test_from_env_oauth_enable_with_server_url(): + """Test BYOT OAuth mode - ATLASSIAN_OAUTH_ENABLE=true with Server URL.""" + with patch.dict( + os.environ, + { + "ATLASSIAN_OAUTH_ENABLE": "true", + "CONFLUENCE_URL": "https://confluence.example.com", + }, + clear=True, + ): + config = ConfluenceConfig.from_env() + assert config.url == "https://confluence.example.com" + assert config.auth_type == "oauth" + assert config.is_cloud is False # Should be Server based on URL diff --git a/tests/unit/jira/test_config.py b/tests/unit/jira/test_config.py index a0c2815d8..ec4889a01 100644 --- a/tests/unit/jira/test_config.py +++ b/tests/unit/jira/test_config.py @@ -203,3 +203,67 @@ def test_is_cloud_oauth_with_cloud_id(): oauth_config=oauth_config, ) assert config.is_cloud is True + + +def test_from_env_oauth_enable_no_url(): + """Test BYOT OAuth mode - ATLASSIAN_OAUTH_ENABLE=true without URL or cloud_id.""" + with patch.dict( + os.environ, + { + "ATLASSIAN_OAUTH_ENABLE": "true", + # No JIRA_URL set + # No ATLASSIAN_OAUTH_CLOUD_ID set + }, + clear=True, + ): + config = JiraConfig.from_env() + assert config.auth_type == "oauth" + assert config.is_cloud is False + + +def test_from_env_oauth_enable_no_url_with_cloud_id(): + """Test BYOT OAuth mode - ATLASSIAN_OAUTH_ENABLE=true without URL but with cloud_id.""" + with patch.dict( + os.environ, + { + "ATLASSIAN_OAUTH_ENABLE": "true", + "ATLASSIAN_OAUTH_CLOUD_ID": "test-cloud-id", + # No JIRA_URL set + }, + clear=True, + ): + config = JiraConfig.from_env() + assert config.auth_type == "oauth" + assert config.is_cloud is True + + +def test_from_env_oauth_enable_with_cloud_url(): + """Test BYOT OAuth mode - ATLASSIAN_OAUTH_ENABLE=true with Cloud URL.""" + with patch.dict( + os.environ, + { + "ATLASSIAN_OAUTH_ENABLE": "true", + "JIRA_URL": "https://test.atlassian.net", + }, + clear=True, + ): + config = JiraConfig.from_env() + assert config.url == "https://test.atlassian.net" + assert config.auth_type == "oauth" + assert config.is_cloud is True + + +def test_from_env_oauth_enable_with_server_url(): + """Test BYOT OAuth mode - ATLASSIAN_OAUTH_ENABLE=true with Server URL.""" + with patch.dict( + os.environ, + { + "ATLASSIAN_OAUTH_ENABLE": "true", + "JIRA_URL": "https://jira.example.com", + }, + clear=True, + ): + config = JiraConfig.from_env() + assert config.url == "https://jira.example.com" + assert config.auth_type == "oauth" + assert config.is_cloud is False diff --git a/tests/unit/utils/test_environment.py b/tests/unit/utils/test_environment.py index 766b11fa6..1e43818ff 100644 --- a/tests/unit/utils/test_environment.py +++ b/tests/unit/utils/test_environment.py @@ -261,3 +261,74 @@ def test_invalid_environment_variables(self, invalid_vars, caplog): _assert_authentication_logs( caplog, "not_configured", ["confluence", "jira"] ) + + def test_oauth_enable_without_urls(self, caplog): + """Test BYOT OAuth mode - ATLASSIAN_OAUTH_ENABLE=true without service URLs.""" + with MockEnvironment.clean_env(): + import os + os.environ["ATLASSIAN_OAUTH_ENABLE"] = "true" + + result = get_available_services() + _assert_service_availability( + result, confluence_expected=True, jira_expected=True + ) + # Should log the minimal OAuth configuration messages + assert_log_contains( + caplog, "INFO", "Using Confluence minimal OAuth configuration - expecting user-provided tokens via headers" + ) + assert_log_contains( + caplog, "INFO", "Using Jira minimal OAuth configuration - expecting user-provided tokens via headers" + ) + + def test_oauth_enable_with_urls(self, caplog): + """Test BYOT OAuth mode - ATLASSIAN_OAUTH_ENABLE=true with service URLs.""" + with MockEnvironment.clean_env(): + import os + os.environ["ATLASSIAN_OAUTH_ENABLE"] = "true" + os.environ["CONFLUENCE_URL"] = "https://test.atlassian.net/wiki" + os.environ["JIRA_URL"] = "https://test.atlassian.net" + + result = get_available_services() + _assert_service_availability( + result, confluence_expected=True, jira_expected=True + ) + # Should log the minimal OAuth configuration messages (overrides URL-based detection) + assert_log_contains( + caplog, "INFO", "Using Confluence minimal OAuth configuration - expecting user-provided tokens via headers" + ) + assert_log_contains( + caplog, "INFO", "Using Jira minimal OAuth configuration - expecting user-provided tokens via headers" + ) + + @pytest.mark.parametrize( + "oauth_enable_value", + ["true", "True", "TRUE", "1", "yes", "YES"] + ) + def test_oauth_enable_value_variations(self, oauth_enable_value, caplog): + """Test various ATLASSIAN_OAUTH_ENABLE value formats.""" + with MockEnvironment.clean_env(): + import os + os.environ["ATLASSIAN_OAUTH_ENABLE"] = oauth_enable_value + + result = get_available_services() + _assert_service_availability( + result, confluence_expected=True, jira_expected=True + ) + + @pytest.mark.parametrize( + "oauth_disable_value", + ["false", "False", "FALSE", "0", "no", "NO", ""] + ) + def test_oauth_enable_disabled_values(self, oauth_disable_value, caplog): + """Test values that should NOT enable BYOT OAuth mode.""" + with MockEnvironment.clean_env(): + import os + os.environ["ATLASSIAN_OAUTH_ENABLE"] = oauth_disable_value + + result = get_available_services() + _assert_service_availability( + result, confluence_expected=False, jira_expected=False + ) + _assert_authentication_logs( + caplog, "not_configured", ["confluence", "jira"] + ) From 1dc330c1c001d1ae75f0a0955bf40f017d0a09fd Mon Sep 17 00:00:00 2001 From: David Neale Date: Wed, 17 Sep 2025 12:53:52 +0100 Subject: [PATCH 2/2] Linting --- tests/unit/confluence/test_config.py | 2 +- tests/unit/jira/test_config.py | 4 ++-- tests/unit/utils/test_environment.py | 26 ++++++++++++++++++-------- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/tests/unit/confluence/test_config.py b/tests/unit/confluence/test_config.py index 3dd2964db..eb7f71f99 100644 --- a/tests/unit/confluence/test_config.py +++ b/tests/unit/confluence/test_config.py @@ -238,7 +238,7 @@ def test_from_env_oauth_enable_with_server_url(): with patch.dict( os.environ, { - "ATLASSIAN_OAUTH_ENABLE": "true", + "ATLASSIAN_OAUTH_ENABLE": "true", "CONFLUENCE_URL": "https://confluence.example.com", }, clear=True, diff --git a/tests/unit/jira/test_config.py b/tests/unit/jira/test_config.py index ec4889a01..a47359d63 100644 --- a/tests/unit/jira/test_config.py +++ b/tests/unit/jira/test_config.py @@ -230,7 +230,7 @@ def test_from_env_oauth_enable_no_url_with_cloud_id(): "ATLASSIAN_OAUTH_CLOUD_ID": "test-cloud-id", # No JIRA_URL set }, - clear=True, + clear=True, ): config = JiraConfig.from_env() assert config.auth_type == "oauth" @@ -258,7 +258,7 @@ def test_from_env_oauth_enable_with_server_url(): with patch.dict( os.environ, { - "ATLASSIAN_OAUTH_ENABLE": "true", + "ATLASSIAN_OAUTH_ENABLE": "true", "JIRA_URL": "https://jira.example.com", }, clear=True, diff --git a/tests/unit/utils/test_environment.py b/tests/unit/utils/test_environment.py index 1e43818ff..39acc0aa7 100644 --- a/tests/unit/utils/test_environment.py +++ b/tests/unit/utils/test_environment.py @@ -266,6 +266,7 @@ def test_oauth_enable_without_urls(self, caplog): """Test BYOT OAuth mode - ATLASSIAN_OAUTH_ENABLE=true without service URLs.""" with MockEnvironment.clean_env(): import os + os.environ["ATLASSIAN_OAUTH_ENABLE"] = "true" result = get_available_services() @@ -274,16 +275,21 @@ def test_oauth_enable_without_urls(self, caplog): ) # Should log the minimal OAuth configuration messages assert_log_contains( - caplog, "INFO", "Using Confluence minimal OAuth configuration - expecting user-provided tokens via headers" + caplog, + "INFO", + "Using Confluence minimal OAuth configuration - expecting user-provided tokens via headers", ) assert_log_contains( - caplog, "INFO", "Using Jira minimal OAuth configuration - expecting user-provided tokens via headers" + caplog, + "INFO", + "Using Jira minimal OAuth configuration - expecting user-provided tokens via headers", ) def test_oauth_enable_with_urls(self, caplog): """Test BYOT OAuth mode - ATLASSIAN_OAUTH_ENABLE=true with service URLs.""" with MockEnvironment.clean_env(): import os + os.environ["ATLASSIAN_OAUTH_ENABLE"] = "true" os.environ["CONFLUENCE_URL"] = "https://test.atlassian.net/wiki" os.environ["JIRA_URL"] = "https://test.atlassian.net" @@ -294,20 +300,24 @@ def test_oauth_enable_with_urls(self, caplog): ) # Should log the minimal OAuth configuration messages (overrides URL-based detection) assert_log_contains( - caplog, "INFO", "Using Confluence minimal OAuth configuration - expecting user-provided tokens via headers" + caplog, + "INFO", + "Using Confluence minimal OAuth configuration - expecting user-provided tokens via headers", ) assert_log_contains( - caplog, "INFO", "Using Jira minimal OAuth configuration - expecting user-provided tokens via headers" + caplog, + "INFO", + "Using Jira minimal OAuth configuration - expecting user-provided tokens via headers", ) @pytest.mark.parametrize( - "oauth_enable_value", - ["true", "True", "TRUE", "1", "yes", "YES"] + "oauth_enable_value", ["true", "True", "TRUE", "1", "yes", "YES"] ) def test_oauth_enable_value_variations(self, oauth_enable_value, caplog): """Test various ATLASSIAN_OAUTH_ENABLE value formats.""" with MockEnvironment.clean_env(): import os + os.environ["ATLASSIAN_OAUTH_ENABLE"] = oauth_enable_value result = get_available_services() @@ -316,13 +326,13 @@ def test_oauth_enable_value_variations(self, oauth_enable_value, caplog): ) @pytest.mark.parametrize( - "oauth_disable_value", - ["false", "False", "FALSE", "0", "no", "NO", ""] + "oauth_disable_value", ["false", "False", "FALSE", "0", "no", "NO", ""] ) def test_oauth_enable_disabled_values(self, oauth_disable_value, caplog): """Test values that should NOT enable BYOT OAuth mode.""" with MockEnvironment.clean_env(): import os + os.environ["ATLASSIAN_OAUTH_ENABLE"] = oauth_disable_value result = get_available_services()