diff --git a/ee/identitymanager/identity_managers/auth0/auth0_authverifier.py b/ee/identitymanager/identity_managers/auth0/auth0_authverifier.py index e351f5a622..5ee4503262 100644 --- a/ee/identitymanager/identity_managers/auth0/auth0_authverifier.py +++ b/ee/identitymanager/identity_managers/auth0/auth0_authverifier.py @@ -1,16 +1,55 @@ +import logging import os import jwt +import requests from fastapi import HTTPException from keep.identitymanager.authenticatedentity import AuthenticatedEntity from keep.identitymanager.authverifierbase import AuthVerifierBase from keep.identitymanager.rbac import Admin as AdminRole +logger = logging.getLogger(__name__) + + +def _discover_jwks_uri(auth_domain: str) -> str: + """Discover the JWKS URI via the OpenID Connect Discovery endpoint. + + Per the OpenID Connect Discovery 1.0 specification + (https://openid.net/specs/openid-connect-discovery-1_0.html#rfc.section.3), + the ``jwks_uri`` should be obtained from the provider's discovery document + at ``{issuer}/.well-known/openid-configuration``. + + Falls back to the Auth0-style ``/.well-known/jwks.json`` path when the + discovery document is unavailable or does not contain ``jwks_uri``. + """ + discovery_url = f"https://{auth_domain}/.well-known/openid-configuration" + try: + resp = requests.get(discovery_url, timeout=10) + resp.raise_for_status() + discovered_uri = resp.json().get("jwks_uri") + if discovered_uri: + return discovered_uri + logger.warning( + "OpenID discovery document at %s did not contain jwks_uri, " + "falling back to /.well-known/jwks.json", + discovery_url, + ) + except Exception: + logger.warning( + "Failed to fetch OpenID discovery document from %s, " + "falling back to /.well-known/jwks.json", + discovery_url, + exc_info=True, + ) + # Fallback: Auth0's conventional JWKS endpoint + return f"https://{auth_domain}/.well-known/jwks.json" + + # Note: cache_keys is set to True to avoid fetching the jwks keys on every request auth_domain = os.environ.get("AUTH0_DOMAIN") if auth_domain: - jwks_uri = f"https://{auth_domain}/.well-known/jwks.json" + jwks_uri = _discover_jwks_uri(auth_domain) jwks_client = jwt.PyJWKClient( jwks_uri, cache_keys=True, headers={"User-Agent": "keep-api"} ) @@ -29,7 +68,7 @@ def __init__(self, scopes: list[str] = []) -> None: self.auth_domain = os.environ.get("AUTH0_DOMAIN") if not self.auth_domain: raise Exception("Missing AUTH0_DOMAIN environment variable") - self.jwks_uri = f"https://{self.auth_domain}/.well-known/jwks.json" + self.jwks_uri = _discover_jwks_uri(self.auth_domain) # Note: cache_keys is set to True to avoid fetching the jwks keys on every request # but it currently caches only per-route. After moving this auth verifier to be a singleton, we can cache it globally self.issuer = f"https://{self.auth_domain}/" diff --git a/keep/providers/jira_provider/jira_provider.py b/keep/providers/jira_provider/jira_provider.py index 01fbe5ae07..3b678794c3 100644 --- a/keep/providers/jira_provider/jira_provider.py +++ b/keep/providers/jira_provider/jira_provider.py @@ -408,7 +408,24 @@ def __create_issue( fields["components"] = [{"name": component} for component in components] if custom_fields: - fields.update(custom_fields) + # Filter out priority field if it's set to "none" or empty + filtered_fields = {} + for key, value in custom_fields.items(): + if key == "priority" and (not value or str(value).lower() in ["none", "", "null"]): + self.logger.info(f"Skipping priority field with value '{value}' as it may not be available on the issue screen") + continue + filtered_fields[key] = value + fields.update(filtered_fields) + + # Also handle priority that might come through kwargs + if kwargs: + filtered_kwargs = {} + for key, value in kwargs.items(): + if key == "priority" and (not value or str(value).lower() in ["none", "", "null"]): + self.logger.info(f"Skipping priority field from kwargs with value '{value}' as it may not be available on the issue screen") + continue + filtered_kwargs[key] = value + fields.update(filtered_kwargs) request_body = {"fields": fields} diff --git a/keep/providers/kafka_provider/kafka_provider.py b/keep/providers/kafka_provider/kafka_provider.py index a567a6e959..1010c1e30c 100644 --- a/keep/providers/kafka_provider/kafka_provider.py +++ b/keep/providers/kafka_provider/kafka_provider.py @@ -76,12 +76,13 @@ def get_client_id_from_caller(self): # Here, you should implement the logic to extract client_id based on the caller. # This can be tricky and might require you to traverse the call stack. # Return a default or None if you can't find it. - import copy - frame = inspect.currentframe() client_id = None while frame: - local_vars = copy.copy(frame.f_locals) + # Use dict() to convert frame.f_locals into a plain dict. + # In Python 3.13+, frame.f_locals returns a FrameLocalsProxy + # which cannot be copied via copy.copy() (pickle fails). + local_vars = dict(frame.f_locals) for var_name, var_value in local_vars.items(): if isinstance(var_value, KafkaProvider): client_id = var_value.context_manager.tenant_id diff --git a/keep/rulesengine/rulesengine.py b/keep/rulesengine/rulesengine.py index ff769b4030..745de1d099 100644 --- a/keep/rulesengine/rulesengine.py +++ b/keep/rulesengine/rulesengine.py @@ -300,6 +300,13 @@ def _get_or_create_incident( # update the incident name template # note that it will be commited later, when the incident is commited incident_name = re.sub(pattern, var_to_replace, incident_name) + # Re-apply the incident prefix after template regeneration. + # The template generates a plain name without the prefix, which + # would otherwise overwrite the prefixed name set during creation + # or the earlier prefix check. + # See: https://github.com/keephq/keep/issues/5450 + if rule.incident_prefix and rule.incident_prefix not in incident_name: + incident_name = f"{rule.incident_prefix}-{existed_incident.running_number} - {incident_name}" # we are done if existed_incident.user_generated_name != incident_name: existed_incident.user_generated_name = incident_name diff --git a/tests/providers/jira_provider/test_jira_priority_fix.py b/tests/providers/jira_provider/test_jira_priority_fix.py new file mode 100644 index 0000000000..6eb6a92f93 --- /dev/null +++ b/tests/providers/jira_provider/test_jira_priority_fix.py @@ -0,0 +1,168 @@ +""" +Test for Jira provider priority field handling. +""" + +import json +import pytest +from unittest.mock import Mock, patch +import responses + +from keep.contextmanager.contextmanager import ContextManager +from keep.providers.jira_provider.jira_provider import JiraProvider +from keep.providers.models.provider_config import ProviderConfig + + +@pytest.fixture +def jira_provider(): + """Fixture for Jira provider.""" + context_manager = ContextManager(tenant_id="test", workflow_id="test") + config = ProviderConfig( + authentication={ + "email": "test@test.com", + "api_token": "test_token", + "host": "https://test.atlassian.net" + }, + name="test-jira" + ) + + provider = JiraProvider(context_manager, "jira", config) + return provider + + +class TestJiraPriorityHandling: + """Test class for Jira priority field handling.""" + + @responses.activate + def test_create_issue_excludes_none_priority(self, jira_provider): + """Test that priority with 'none' value is excluded from request.""" + + # Mock the create issue endpoint + responses.add( + responses.POST, + "https://test.atlassian.net/rest/api/2/issue", + json={"id": "123", "key": "TEST-123", "self": "https://test.atlassian.net/rest/api/2/issue/123"}, + status=201 + ) + + # Call the create issue method with priority: "none" + result = jira_provider._JiraProvider__create_issue( + project_key="TEST", + summary="Test Issue", + description="Test Description", + issue_type="Bug", + custom_fields={"priority": "none"} + ) + + # Verify the request was made without priority field + assert len(responses.calls) == 1 + request_body = json.loads(responses.calls[0].request.body) + + # Priority should not be in the fields + assert "priority" not in request_body["fields"] + assert "summary" in request_body["fields"] + assert request_body["fields"]["summary"] == "Test Issue" + + @responses.activate + def test_create_issue_excludes_empty_priority(self, jira_provider): + """Test that priority with empty value is excluded from request.""" + + responses.add( + responses.POST, + "https://test.atlassian.net/rest/api/2/issue", + json={"id": "124", "key": "TEST-124", "self": "https://test.atlassian.net/rest/api/2/issue/124"}, + status=201 + ) + + # Test with empty string priority + jira_provider._JiraProvider__create_issue( + project_key="TEST", + summary="Test Issue", + description="Test Description", + issue_type="Bug", + custom_fields={"priority": ""} + ) + + request_body = json.loads(responses.calls[0].request.body) + assert "priority" not in request_body["fields"] + + @responses.activate + def test_create_issue_excludes_null_priority(self, jira_provider): + """Test that priority with null value is excluded from request.""" + + responses.add( + responses.POST, + "https://test.atlassian.net/rest/api/2/issue", + json={"id": "125", "key": "TEST-125", "self": "https://test.atlassian.net/rest/api/2/issue/125"}, + status=201 + ) + + # Test with None priority + jira_provider._JiraProvider__create_issue( + project_key="TEST", + summary="Test Issue", + description="Test Description", + issue_type="Bug", + custom_fields={"priority": None} + ) + + request_body = json.loads(responses.calls[0].request.body) + assert "priority" not in request_body["fields"] + + @responses.activate + def test_create_issue_includes_valid_priority(self, jira_provider): + """Test that valid priority values are included in request.""" + + responses.add( + responses.POST, + "https://test.atlassian.net/rest/api/2/issue", + json={"id": "126", "key": "TEST-126", "self": "https://test.atlassian.net/rest/api/2/issue/126"}, + status=201 + ) + + # Test with valid priority + jira_provider._JiraProvider__create_issue( + project_key="TEST", + summary="Test Issue", + description="Test Description", + issue_type="Bug", + custom_fields={"priority": {"name": "High"}} + ) + + request_body = json.loads(responses.calls[0].request.body) + assert "priority" in request_body["fields"] + assert request_body["fields"]["priority"] == {"name": "High"} + + @responses.activate + def test_create_issue_preserves_other_custom_fields(self, jira_provider): + """Test that other custom fields are preserved when priority is filtered.""" + + responses.add( + responses.POST, + "https://test.atlassian.net/rest/api/2/issue", + json={"id": "127", "key": "TEST-127", "self": "https://test.atlassian.net/rest/api/2/issue/127"}, + status=201 + ) + + # Test with priority: none and other custom fields + jira_provider._JiraProvider__create_issue( + project_key="TEST", + summary="Test Issue", + description="Test Description", + issue_type="Bug", + custom_fields={ + "priority": "none", + "customfield_12345": "Custom Value", + "environment": "Production" + } + ) + + request_body = json.loads(responses.calls[0].request.body) + + # Priority should be filtered out + assert "priority" not in request_body["fields"] + + # Other custom fields should be preserved + assert "customfield_12345" in request_body["fields"] + assert "environment" in request_body["fields"] + assert request_body["fields"]["customfield_12345"] == "Custom Value" + assert request_body["fields"]["environment"] == "Production" \ No newline at end of file