Skip to content

Commit 690cd30

Browse files
azulusclaude
andauthored
feat(seer): Add signed viewer context header to Seer API requests (#109626)
Add an optional `viewer_context` parameter to `make_signed_seer_api_request` that allows callers to pass `organization_id` and/or `user_id`. When provided, these are serialized as JSON into an `X-Viewer-Context` header and HMAC-SHA256 signed into a companion `X-Viewer-Context-Signature` header using the shared Seer secret. This gives Seer a verified way to know which organization and user originated a request, enabling access control and audit logging on their side. No callers pass this yet — this just adds the plumbing. --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6e2dd0f commit 690cd30

File tree

1 file changed

+33
-1
lines changed

1 file changed

+33
-1
lines changed

src/sentry/seer/signed_seer_api.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@
1212
from sentry.net.http import connection_from_url
1313
from sentry.utils import metrics
1414

15+
16+
class SeerViewerContext(TypedDict, total=False):
17+
organization_id: int
18+
user_id: int
19+
20+
1521
logger = logging.getLogger(__name__)
1622

1723

@@ -37,6 +43,7 @@ def make_signed_seer_api_request(
3743
retries: int | None | Retry = None,
3844
metric_tags: dict[str, Any] | None = None,
3945
method: str = "POST",
46+
viewer_context: SeerViewerContext | None = None,
4047
) -> BaseHTTPResponse:
4148
host = connection_pool.host
4249
if connection_pool.port:
@@ -47,6 +54,22 @@ def make_signed_seer_api_request(
4754

4855
auth_headers = sign_with_seer_secret(body)
4956

57+
headers: dict[str, str] = {
58+
"content-type": "application/json;charset=utf-8",
59+
**auth_headers,
60+
}
61+
62+
if viewer_context:
63+
if settings.SEER_API_SHARED_SECRET:
64+
context_bytes = orjson.dumps(viewer_context)
65+
context_signature = sign_viewer_context(context_bytes)
66+
headers["X-Viewer-Context"] = context_bytes.decode("utf-8")
67+
headers["X-Viewer-Context-Signature"] = context_signature
68+
else:
69+
logger.warning(
70+
"settings.SEER_API_SHARED_SECRET is not set. Unable to sign viewer context for call to Seer."
71+
)
72+
5073
options: dict[str, Any] = {}
5174
if timeout:
5275
options["timeout"] = timeout
@@ -62,7 +85,7 @@ def make_signed_seer_api_request(
6285
method,
6386
parsed.path,
6487
body=body,
65-
headers={"content-type": "application/json;charset=utf-8", **auth_headers},
88+
headers=headers,
6689
**options,
6790
)
6891

@@ -382,3 +405,12 @@ def sign_with_seer_secret(body: bytes) -> dict[str, str]:
382405
"settings.SEER_API_SHARED_SECRET is not set. Unable to add auth headers for call to Seer."
383406
)
384407
return auth_headers
408+
409+
410+
def sign_viewer_context(context_bytes: bytes) -> str:
411+
"""Sign the viewer context payload with the shared secret."""
412+
return hmac.new(
413+
settings.SEER_API_SHARED_SECRET.encode("utf-8"),
414+
context_bytes,
415+
hashlib.sha256,
416+
).hexdigest()

0 commit comments

Comments
 (0)