Skip to content

Commit

Permalink
First pass converting seer api calls to using signature (#72486)
Browse files Browse the repository at this point in the history
Gotta adjust some tests.

This addresses the need to sign not just to sentry, but from sentry.
Unfortunately, given the way we did signing originally, we can't reuse
the existing secret due to asymmetry. However, good news is that all of
this may be replaceable with grpc in the near future. This addresses the
immediate security issues and puts us on track to remove most of this
later.

See https://github.com/getsentry/seer/pull/774/files
  • Loading branch information
corps committed Jun 18, 2024
1 parent 6920f5f commit d3fbd9b
Show file tree
Hide file tree
Showing 19 changed files with 314 additions and 104 deletions.
85 changes: 51 additions & 34 deletions src/sentry/api/endpoints/group_ai_autofix.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from sentry.autofix.utils import get_autofix_repos_from_project_code_mappings
from sentry.models.group import Group
from sentry.models.user import User
from sentry.seer.signed_seer_api import sign_with_seer_secret
from sentry.services.hybrid_cloud.user.service import user_service
from sentry.types.ratelimit import RateLimit, RateLimitCategory

Expand Down Expand Up @@ -85,47 +86,63 @@ def _call_autofix(
instruction: str,
timeout_secs: int,
):
response = requests.post(
f"{settings.SEER_AUTOFIX_URL}/v1/automation/autofix/start",
data=orjson.dumps(
{
"organization_id": group.organization.id,
"project_id": group.project.id,
"repos": repos,
"issue": {
"id": group.id,
"title": group.title,
"short_id": group.qualified_short_id,
"events": [serialized_event],
},
"instruction": instruction,
"timeout_secs": timeout_secs,
"last_updated": datetime.now().isoformat(),
"invoking_user": (
{
"id": user.id,
"display_name": user.get_display_name(),
}
if not isinstance(user, AnonymousUser)
else None
),
path = "/v1/automation/autofix/start"
body = orjson.dumps(
{
"organization_id": group.organization.id,
"project_id": group.project.id,
"repos": repos,
"issue": {
"id": group.id,
"title": group.title,
"short_id": group.qualified_short_id,
"events": [serialized_event],
},
option=orjson.OPT_NON_STR_KEYS,
),
headers={"content-type": "application/json;charset=utf-8"},
"instruction": instruction,
"timeout_secs": timeout_secs,
"last_updated": datetime.now().isoformat(),
"invoking_user": (
{
"id": user.id,
"display_name": user.get_display_name(),
}
if not isinstance(user, AnonymousUser)
else None
),
},
option=orjson.OPT_NON_STR_KEYS,
)
response = requests.post(
f"{settings.SEER_AUTOFIX_URL}{path}",
data=body,
headers={
"content-type": "application/json;charset=utf-8",
**sign_with_seer_secret(
url=f"{settings.SEER_AUTOFIX_URL}{path}",
body=body,
),
},
)

response.raise_for_status()

def _call_get_autofix_state(self, group_id: int) -> dict[str, Any] | None:
path = "/v1/automation/autofix/state"
body = orjson.dumps(
{
"group_id": group_id,
}
)
response = requests.post(
f"{settings.SEER_AUTOFIX_URL}/v1/automation/autofix/state",
data=orjson.dumps(
{
"group_id": group_id,
}
),
headers={"content-type": "application/json;charset=utf-8"},
f"{settings.SEER_AUTOFIX_URL}{path}",
data=body,
headers={
"content-type": "application/json;charset=utf-8",
**sign_with_seer_secret(
url=f"{settings.SEER_AUTOFIX_URL}{path}",
body=body,
),
},
)

response.raise_for_status()
Expand Down
23 changes: 16 additions & 7 deletions src/sentry/api/endpoints/group_autofix_setup_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from sentry.models.group import Group
from sentry.models.organization import Organization
from sentry.models.project import Project
from sentry.seer.signed_seer_api import sign_with_seer_secret
from sentry.services.hybrid_cloud.integration import integration_service

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -67,15 +68,23 @@ def get_repos_and_access(project: Project) -> list[dict]:
repos = get_autofix_repos_from_project_code_mappings(project)

repos_and_access: list[dict] = []
path = "/v1/automation/codebase/repo/check-access"
for repo in repos:
body = orjson.dumps(
{
"repo": repo,
}
)
response = requests.post(
f"{settings.SEER_AUTOFIX_URL}/v1/automation/codebase/repo/check-access",
data=orjson.dumps(
{
"repo": repo,
}
),
headers={"content-type": "application/json;charset=utf-8"},
f"{settings.SEER_AUTOFIX_URL}{path}",
data=body,
headers={
"content-type": "application/json;charset=utf-8",
**sign_with_seer_secret(
url=f"{settings.SEER_AUTOFIX_URL}{path}",
body=body,
),
},
)

response.raise_for_status()
Expand Down
3 changes: 2 additions & 1 deletion src/sentry/api/endpoints/group_autofix_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ def post(self, request: Request, group: Group) -> Response:
data={"error": "You must be authenticated to use this endpoint"},
)

path = "/v1/automation/autofix/update"
response = requests.post(
f"{settings.SEER_AUTOFIX_URL}/v1/automation/autofix/update",
f"{settings.SEER_AUTOFIX_URL}{path}",
data=orjson.dumps(
{
**request.data,
Expand Down
5 changes: 2 additions & 3 deletions src/sentry/api/endpoints/organization_profiling_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from sentry.models.organization import Organization
from sentry.search.events.builder import ProfileTopFunctionsTimeseriesQueryBuilder
from sentry.search.events.types import QueryBuilderConfig
from sentry.seer.breakpoints import BreakpointData, detect_breakpoints
from sentry.seer.breakpoints import BreakpointData, BreakpointRequest, detect_breakpoints
from sentry.snuba import functions
from sentry.snuba.dataset import Dataset
from sentry.snuba.referrer import Referrer
Expand Down Expand Up @@ -163,7 +163,7 @@ def get_trends_data(stats_data) -> list[BreakpointData]:
if not stats_data:
return []

trends_request = {
trends_request: BreakpointRequest = {
"data": {
k: {
"data": v[data["function"]]["data"],
Expand All @@ -182,7 +182,6 @@ def get_trends_data(stats_data) -> list[BreakpointData]:
if v[data["function"]]["data"]
},
"sort": data["trend"].as_sort(),
"trendFunction": data["function"],
}

return detect_breakpoints(trends_request)["data"]
Expand Down
27 changes: 18 additions & 9 deletions src/sentry/api/endpoints/project_autofix_create_codebase_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from sentry.api.bases.project import ProjectEndpoint, ProjectPermission
from sentry.autofix.utils import get_autofix_repos_from_project_code_mappings
from sentry.models.project import Project
from sentry.seer.signed_seer_api import sign_with_seer_secret

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -41,18 +42,26 @@ def post(self, request: Request, project: Project) -> Response:
Create a codebase index for for a project's repositories, uses the code mapping to determine which repositories to index
"""
repos = get_autofix_repos_from_project_code_mappings(project)
path = "/v1/automation/codebase/index/create"

for repo in repos:
body = orjson.dumps(
{
"organization_id": project.organization.id,
"project_id": project.id,
"repo": repo,
}
)
response = requests.post(
f"{settings.SEER_AUTOFIX_URL}/v1/automation/codebase/index/create",
data=orjson.dumps(
{
"organization_id": project.organization.id,
"project_id": project.id,
"repo": repo,
}
),
headers={"content-type": "application/json;charset=utf-8"},
f"{settings.SEER_AUTOFIX_URL}{path}",
data=body,
headers={
"content-type": "application/json;charset=utf-8",
**sign_with_seer_secret(
url=f"{settings.SEER_AUTOFIX_URL}{path}",
body=body,
),
},
)

response.raise_for_status()
Expand Down
29 changes: 19 additions & 10 deletions src/sentry/api/helpers/autofix.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from django.conf import settings

from sentry.autofix.utils import get_autofix_repos_from_project_code_mappings
from sentry.seer.signed_seer_api import sign_with_seer_secret


class AutofixCodebaseIndexingStatus(str, enum.Enum):
Expand All @@ -20,18 +21,26 @@ def get_project_codebase_indexing_status(project):
return None

statuses = []
path = "/v1/automation/codebase/index/status"
for repo in repos:
body = orjson.dumps(
{
"organization_id": project.organization.id,
"project_id": project.id,
"repo": repo,
},
option=orjson.OPT_UTC_Z,
)
response = requests.post(
f"{settings.SEER_AUTOFIX_URL}/v1/automation/codebase/index/status",
data=orjson.dumps(
{
"organization_id": project.organization.id,
"project_id": project.id,
"repo": repo,
},
option=orjson.OPT_UTC_Z,
),
headers={"content-type": "application/json;charset=utf-8"},
f"{settings.SEER_AUTOFIX_URL}{path}",
data=body,
headers={
"content-type": "application/json;charset=utf-8",
**sign_with_seer_secret(
url=f"{settings.SEER_AUTOFIX_URL}{path}",
body=body,
),
},
)

response.raise_for_status()
Expand Down
25 changes: 17 additions & 8 deletions src/sentry/autofix/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from sentry.integrations.utils.code_mapping import get_sorted_code_mapping_configs
from sentry.models.project import Project
from sentry.models.repository import Repository
from sentry.seer.signed_seer_api import sign_with_seer_secret
from sentry.utils import json


Expand Down Expand Up @@ -51,15 +52,23 @@ def get_autofix_repos_from_project_code_mappings(project: Project) -> list[dict]


def get_autofix_state_from_pr_id(provider: str, pr_id: int) -> AutofixState | None:
path = "/v1/automation/autofix/state/pr"
body = json.dumps(
{
"provider": provider,
"pr_id": pr_id,
}
).encode("utf-8")
response = requests.post(
f"{settings.SEER_AUTOFIX_URL}/v1/automation/autofix/state/pr",
data=json.dumps(
{
"provider": provider,
"pr_id": pr_id,
}
),
headers={"content-type": "application/json;charset=utf-8"},
f"{settings.SEER_AUTOFIX_URL}{path}",
data=body,
headers={
"content-type": "application/json;charset=utf-8",
**sign_with_seer_secret(
url=f"{settings.SEER_AUTOFIX_URL}{path}",
body=body,
),
},
)

response.raise_for_status()
Expand Down
5 changes: 4 additions & 1 deletion src/sentry/conf/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -673,8 +673,11 @@ def SOCIAL_AUTH_DEFAULT_USERNAME() -> str:
# Timeout for RPC requests between regions
RPC_TIMEOUT = 5.0

# Shared secret used to sign cross-region RPC requests with the seer microservice.
# TODO: Replace both of these secrets with mutual TLS and simplify our rpc channels.
# Shared secret used to sign cross-region RPC requests from the seer microservice.
SEER_RPC_SHARED_SECRET: list[str] | None = None
# Shared secret used to sign cross-region RPC requests to the seer microservice.
SEER_API_SHARED_SECRET: str = ""

# The protocol, host and port for control silo
# Usecases include sending requests to the Integration Proxy Endpoint and RPC requests.
Expand Down
15 changes: 15 additions & 0 deletions src/sentry/options/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -2550,3 +2550,18 @@
default=50,
flags=FLAG_AUTOMATOR_MODIFIABLE,
)

# Enable orjson in the occurrence_consumer.process_[message|batch]
register(
"issues.occurrence_consumer.use_orjson",
type=Bool,
default=False,
flags=FLAG_AUTOMATOR_MODIFIABLE,
)

# Controls the rate of using the sentry api shared secret for communicating to sentry.
register(
"seer.api.use-shared-secret",
default=0.0,
flags=FLAG_AUTOMATOR_MODIFIABLE,
)
38 changes: 32 additions & 6 deletions src/sentry/seer/breakpoints.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import logging
from typing import TypedDict
from collections.abc import Mapping
from typing import NotRequired, TypedDict

import sentry_sdk
from django.conf import settings
from urllib3 import Retry

from sentry.net.http import connection_from_url
from sentry.seer.signed_seer_api import make_signed_seer_api_request
from sentry.utils import json

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -44,12 +46,36 @@ class BreakpointResponse(TypedDict):
)


def detect_breakpoints(breakpoint_request) -> BreakpointResponse:
response = seer_breakpoint_connection_pool.urlopen(
"POST",
# TODO: Source these from shared schema repository
class BreakpointRequest(TypedDict):
data: "Mapping[str, BreakpointTransaction]"
sort: NotRequired[str]
allow_midpoint: NotRequired[str]
validate_tail_hours: NotRequired[int]
trend_percentage: NotRequired[float]
min_change: NotRequired[float]


class BreakpointTransaction(TypedDict):
data: "list[SnubaTSEntry]"
request_start: int
request_end: int
data_start: int
data_end: int


class SnubaMetadata(TypedDict):
count: float


SnubaTSEntry = tuple[int, tuple[SnubaMetadata]]


def detect_breakpoints(breakpoint_request: BreakpointRequest) -> BreakpointResponse:
response = make_signed_seer_api_request(
seer_breakpoint_connection_pool,
"/trends/breakpoint-detector",
body=json.dumps(breakpoint_request),
headers={"content-type": "application/json;charset=utf-8"},
json.dumps(breakpoint_request).encode("utf-8"),
)

if response.status >= 200 and response.status < 300:
Expand Down
Loading

0 comments on commit d3fbd9b

Please sign in to comment.