Files
sentinel/tests/sentry/api/endpoints/test_relay_projectconfigs.py
2024-02-15 17:01:50 +00:00

372 lines
12 KiB
Python

from __future__ import annotations
import re
from typing import Any
from uuid import uuid4
import pytest
from django.urls import reverse
from sentry_relay.auth import generate_key_pair
from sentry import quotas
from sentry.constants import ObjectStatus
from sentry.models.project import Project
from sentry.models.relay import Relay
from sentry.testutils.helpers import Feature
from sentry.testutils.pytest.fixtures import django_db_all
from sentry.utils import json, safe
_date_regex = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z$")
def _get_all_keys(config):
for key in config:
yield key
if key == "breakdownsV2":
# Breakdown keys are not field names and may contain underscores,
# e.g. span_ops
continue
if isinstance(config[key], dict):
for key in _get_all_keys(config[key]):
yield key
@pytest.fixture
def key_pair():
return generate_key_pair()
@pytest.fixture
def public_key(key_pair):
return key_pair[1]
@pytest.fixture
def private_key(key_pair):
return key_pair[0]
@pytest.fixture
def relay_id():
return str(uuid4())
@pytest.fixture
def relay(relay_id, public_key):
return Relay.objects.create(relay_id=relay_id, public_key=str(public_key), is_internal=True)
@pytest.fixture(autouse=True)
def setup_relay(default_project):
default_project.update_option("sentry:scrub_ip_address", True)
@pytest.fixture
def call_endpoint(client, relay, private_key, default_project):
def inner(full_config, projects=None):
path = reverse("sentry-api-0-relay-projectconfigs")
if projects is None:
projects = [str(default_project.id)]
if full_config is None:
raw_json, signature = private_key.pack({"projects": projects})
else:
raw_json, signature = private_key.pack(
{"projects": projects, "fullConfig": full_config}
)
resp = client.post(
path,
data=raw_json,
content_type="application/json",
HTTP_X_SENTRY_RELAY_ID=relay.relay_id,
HTTP_X_SENTRY_RELAY_SIGNATURE=signature,
)
return json.loads(resp.content), resp.status_code
return inner
@pytest.fixture
def add_org_key(default_organization, relay):
default_organization.update_option(
"sentry:trusted-relays", [{"public_key": relay.public_key, "name": "main-relay"}]
)
@pytest.fixture
def no_internal_networks(monkeypatch):
"""
Disable is_internal_ip functionality (make all requests appear to be from external networks)
"""
monkeypatch.setattr("sentry.auth.system.INTERNAL_NETWORKS", ())
@django_db_all
def test_internal_relays_should_receive_minimal_configs_if_they_do_not_explicitly_ask_for_full_config(
call_endpoint, default_project
):
result, status_code = call_endpoint(full_config=False)
assert status_code < 400
# Sweeping assertion that we do not have any snake_case in that config.
# Might need refining.
assert not {x for x in _get_all_keys(result) if "-" in x or "_" in x}
cfg = safe.get_path(result, "configs", str(default_project.id))
assert safe.get_path(cfg, "config", "filterSettings") is None
assert safe.get_path(cfg, "config", "groupingConfig") is None
@django_db_all
def test_internal_relays_should_receive_full_configs(
call_endpoint, default_project, default_projectkey
):
result, status_code = call_endpoint(full_config=True)
assert status_code < 400
# Sweeping assertion that we do not have any snake_case in that config.
# Might need refining.
assert not {x for x in _get_all_keys(result) if "-" in x or "_" in x}
cfg = safe.get_path(result, "configs", str(default_project.id))
assert safe.get_path(cfg, "disabled") is False
(public_key,) = cfg["publicKeys"]
assert public_key["publicKey"] == default_projectkey.public_key
assert public_key["isEnabled"]
assert safe.get_path(cfg, "slug") == default_project.slug
last_change = safe.get_path(cfg, "lastChange")
assert _date_regex.match(last_change) is not None
last_fetch = safe.get_path(cfg, "lastFetch")
assert _date_regex.match(last_fetch) is not None
assert safe.get_path(cfg, "organizationId") == default_project.organization.id
assert safe.get_path(cfg, "projectId") == default_project.id
assert safe.get_path(cfg, "slug") == default_project.slug
assert safe.get_path(cfg, "rev") is not None
assert safe.get_path(cfg, "config", "trustedRelays") == []
assert safe.get_path(cfg, "config", "filterSettings") is not None
assert safe.get_path(cfg, "config", "groupingConfig", "enhancements") is not None
assert safe.get_path(cfg, "config", "groupingConfig", "id") is not None
assert safe.get_path(cfg, "config", "piiConfig", "applications") is None
assert safe.get_path(cfg, "config", "piiConfig", "rules") is None
assert safe.get_path(cfg, "config", "datascrubbingSettings", "scrubData") is True
assert safe.get_path(cfg, "config", "datascrubbingSettings", "scrubDefaults") is True
assert safe.get_path(cfg, "config", "datascrubbingSettings", "scrubIpAddresses") is True
assert safe.get_path(cfg, "config", "datascrubbingSettings", "sensitiveFields") == []
assert safe.get_path(cfg, "config", "quotas") is None
# Event retention depends on settings, so assert the actual value.
assert safe.get_path(cfg, "config", "eventRetention") == quotas.backend.get_event_retention(
default_project.organization
)
@django_db_all
def test_relays_dyamic_sampling(client, call_endpoint, default_project, dyn_sampling_data):
"""
Tests that dynamic sampling configuration set in project details are retrieved in relay configs
"""
with Feature(
{
"organizations:dynamic-sampling": True,
}
):
result, status_code = call_endpoint(full_config=False)
assert status_code < 400
dynamic_sampling = safe.get_path(
result, "configs", str(default_project.id), "config", "sampling"
)
assert dynamic_sampling == {"version": 2, "rules": []}
@django_db_all
def test_trusted_external_relays_should_not_be_able_to_request_full_configs(
add_org_key, call_endpoint, no_internal_networks
):
result, status_code = call_endpoint(full_config=True)
assert status_code == 403
@django_db_all
def test_when_not_sending_full_config_info_into_a_internal_relay_a_restricted_config_is_returned(
call_endpoint, default_project
):
result, status_code = call_endpoint(full_config=None)
assert status_code < 400
cfg = safe.get_path(result, "configs", str(default_project.id))
assert safe.get_path(cfg, "config", "filterSettings") is None
assert safe.get_path(cfg, "config", "groupingConfig") is None
@django_db_all
def test_when_not_sending_full_config_info_into_an_external_relay_a_restricted_config_is_returned(
call_endpoint, add_org_key, relay, default_project
):
relay.is_internal = False
relay.save()
result, status_code = call_endpoint(full_config=None)
assert status_code < 400
cfg = safe.get_path(result, "configs", str(default_project.id))
assert safe.get_path(cfg, "config", "filterSettings") is None
assert safe.get_path(cfg, "config", "groupingConfig") is None
@django_db_all
def test_trusted_external_relays_should_receive_minimal_configs(
relay, add_org_key, call_endpoint, default_project, default_projectkey
):
relay.is_internal = False
relay.save()
result, status_code = call_endpoint(full_config=False)
assert status_code < 400
cfg = safe.get_path(result, "configs", str(default_project.id))
assert safe.get_path(cfg, "disabled") is False
(public_key,) = cfg["publicKeys"]
assert public_key["publicKey"] == default_projectkey.public_key
assert public_key["numericId"] == default_projectkey.id
assert public_key["isEnabled"]
assert "quotas" not in public_key
assert safe.get_path(cfg, "slug") == default_project.slug
last_change = safe.get_path(cfg, "lastChange")
assert _date_regex.match(last_change) is not None
last_fetch = safe.get_path(cfg, "lastFetch")
assert _date_regex.match(last_fetch) is not None
assert safe.get_path(cfg, "organizationId") == default_project.organization.id
assert safe.get_path(cfg, "projectId") == default_project.id
assert safe.get_path(cfg, "slug") == default_project.slug
assert safe.get_path(cfg, "rev") is not None
assert safe.get_path(cfg, "config", "trustedRelays") == [relay.public_key]
assert safe.get_path(cfg, "config", "filterSettings") is None
assert safe.get_path(cfg, "config", "groupingConfig") is None
assert safe.get_path(cfg, "config", "datascrubbingSettings", "scrubData") is not None
assert safe.get_path(cfg, "config", "datascrubbingSettings", "scrubIpAddresses") is not None
assert safe.get_path(cfg, "config", "piiConfig", "rules") is None
assert safe.get_path(cfg, "config", "piiConfig", "applications") is None
assert safe.get_path(cfg, "config", "quotas") is None
@django_db_all
def test_untrusted_external_relays_should_not_receive_configs(
call_endpoint, default_project, no_internal_networks
):
result, status_code = call_endpoint(full_config=False)
assert status_code < 400
cfg = result["configs"][str(default_project.id)]
assert cfg["disabled"]
@pytest.fixture
def projectconfig_cache_set(monkeypatch):
calls: list[dict[str, Any]] = []
monkeypatch.setattr("sentry.relay.projectconfig_cache.backend.set_many", calls.append)
return calls
@django_db_all
def test_relay_projectconfig_cache_minimal_config(
call_endpoint, default_project, projectconfig_cache_set, task_runner
):
"""
When a relay fetches a minimal config, that config should not end up in Redis.
"""
with task_runner():
result, status_code = call_endpoint(full_config=False)
assert status_code < 400
assert not projectconfig_cache_set
@django_db_all
def test_relay_projectconfig_cache_full_config(
call_endpoint, default_project, projectconfig_cache_set, task_runner
):
"""
When a relay fetches a full config, that config should end up in Redis.
"""
with task_runner():
result, status_code = call_endpoint(full_config=True)
assert status_code < 400
(http_cfg,) = result["configs"].values()
(call,) = projectconfig_cache_set
assert len(call) == 1
redis_cfg = call[str(default_project.id)]
del http_cfg["lastFetch"]
del http_cfg["lastChange"]
del redis_cfg["lastFetch"]
del redis_cfg["lastChange"]
assert redis_cfg == http_cfg
@django_db_all
def test_relay_nonexistent_project(call_endpoint, projectconfig_cache_set, task_runner):
wrong_id = max(p.id for p in Project.objects.all()) + 1
with task_runner():
result, status_code = call_endpoint(full_config=True, projects=[wrong_id])
assert status_code < 400
(http_cfg,) = result["configs"].values()
assert http_cfg == {"disabled": True}
assert projectconfig_cache_set == [{str(wrong_id): http_cfg}]
@django_db_all
def test_relay_disabled_project(
call_endpoint, default_project, projectconfig_cache_set, task_runner
):
default_project.update(status=ObjectStatus.PENDING_DELETION)
wrong_id = default_project.id
with task_runner():
result, status_code = call_endpoint(full_config=True, projects=[wrong_id])
assert status_code < 400
(http_cfg,) = result["configs"].values()
assert http_cfg == {"disabled": True}
assert projectconfig_cache_set == [{str(wrong_id): http_cfg}]
@django_db_all
def test_health_check_filters(call_endpoint, add_org_key, relay, default_project):
"""
Test health check filter (aka ignoreTransactions)
"""
relay.save()
default_project.update_option("filters:filtered-transaction", "1")
result, status_code = call_endpoint(full_config=True)
assert status_code < 400
filter_settings = safe.get_path(
result, "configs", str(default_project.id), "config", "filterSettings"
)
assert filter_settings is not None
assert "ignoreTransactions" in filter_settings