Skip to content

Commit

Permalink
ref(hc): Make the auth middleware use hybrid cloud authentication. (#…
Browse files Browse the repository at this point in the history
…53285)

Uses hybrid cloud level authentication in django views by default.
  • Loading branch information
corps authored and armenzg committed Jul 24, 2023
1 parent 8a51f74 commit aa1d06b
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 12 deletions.
4 changes: 4 additions & 0 deletions src/sentry/middleware/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ def process_request(self, request: Request):
auth_result = auth_service.authenticate(request=authentication_request_from(request))
request.user_from_signed_request = auth_result.user_from_signed_request

# Simulate accessing attributes on the session to trigger side effects related to doing so.
for attr in auth_result.accessed:
request.session[attr]

if auth_result.auth is not None:
request.auth = auth_result.auth
if auth_result.expired:
Expand Down
11 changes: 10 additions & 1 deletion src/sentry/services/hybrid_cloud/auth/impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,9 @@ def authenticate(self, *, request: AuthenticationRequest) -> MiddlewareAuthentic
auth = AuthenticatedToken.from_token(fake_request.auth)

result = MiddlewareAuthenticationResponse(
auth=auth, user_from_signed_request=fake_request.user_from_signed_request
auth=auth,
user_from_signed_request=fake_request.user_from_signed_request,
accessed=fake_request.session._accessed,
)

if expired_user is not None:
Expand Down Expand Up @@ -236,11 +238,18 @@ def get_auth_providers(self, organization_id: int) -> List[RpcAuthProvider]:

class FakeRequestDict:
d: Mapping[str, str | bytes | None]
_accessed: set[str]

def __init__(self, **d: Any):
self.d = d
self._accessed = set()

@property
def accessed(self) -> bool:
return bool(self._accessed)

def __getitem__(self, item: str) -> str | bytes:
self._accessed.add(item)
result = self.d[item]
if result is None:
raise KeyError(f"Key '{item!r}' does not exist")
Expand Down
51 changes: 41 additions & 10 deletions src/sentry/services/hybrid_cloud/auth/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,21 @@
import base64
import contextlib
from enum import IntEnum
from typing import TYPE_CHECKING, Any, Dict, Generator, List, Mapping, Optional, Tuple, Type, Union

from typing import (
TYPE_CHECKING,
Any,
Dict,
Generator,
List,
Mapping,
Optional,
Set,
Tuple,
Type,
Union,
)

from django.contrib.sessions.backends.base import SessionBase
from pydantic.fields import Field
from rest_framework.authentication import BaseAuthentication
from rest_framework.request import Request
Expand Down Expand Up @@ -123,31 +136,48 @@ class AuthenticationRequest(RpcModel):
user_id: Optional[str] = None
user_hash: Optional[str] = None
nonce: Optional[str] = None

remote_addr: Optional[str] = None
signature: Optional[str] = None
absolute_url: str = ""
absolute_url_root: str = ""
path: str = ""
authorization_b64: Optional[str] = None

@classmethod
def get_attributes_of_session_keys(cls) -> Mapping[str, Any]:
return dict(
backend="_auth_user_backend",
user_id="_auth_user_id",
user_hash="_auth_user_hash",
nonce="_nonce",
)

def apply_from_session(self, session: SessionBase) -> "AuthenticationRequest":
"""
Copies over attributes from session without changing the existing value for session.accessed
Modifies self in place and returns it.
"""
orig = session.accessed
for attr, session_key in self.get_attributes_of_session_keys().items():
setattr(self, attr, session.get(session_key, None))
session.accessed = orig
return self


def authentication_request_from(request: Request) -> AuthenticationRequest:
from sentry.utils.linksign import find_signature

return AuthenticationRequest(
sentry_relay_id=get_header_relay_id(request),
sentry_relay_signature=get_header_relay_signature(request),
backend=request.session.get("_auth_user_backend", None),
user_id=request.session.get("_auth_user_id", None),
user_hash=request.session.get("_auth_user_hash", None),
nonce=request.session.get("_nonce", None),
remote_addr=request.META["REMOTE_ADDR"],
signature=find_signature(request),
absolute_url=request.build_absolute_uri(),
absolute_url_root=request.build_absolute_uri("/"),
path=request.path,
authorization_b64=_normalize_to_b64(request.META.get("HTTP_AUTHORIZATION")),
)
).apply_from_session(request.session)


class AuthenticatedToken(RpcModel):
Expand Down Expand Up @@ -229,7 +259,7 @@ def _get_user(self) -> Union[RpcUser, "AnonymousUser"]:
return self.user or AnonymousUser()

@contextlib.contextmanager
def applied_to_request(self, request: Any = None) -> Generator[None, None, None]:
def applied_to_request(self, request: Any = None) -> Generator[Any, None, None]:
"""
Some code still reaches for the global 'env' object when determining user or auth behaviors. This bleeds the
current request context into that code, but makes it difficult to carry RPC authentication context in an
Expand All @@ -244,7 +274,7 @@ def applied_to_request(self, request: Any = None) -> Generator[None, None, None]
if request is None:
# Contexts that lack a request
# Note -- if a request is setup in the env after this context manager, you run the risk of bugs.
yield
yield request
return

has_user = hasattr(request, "user")
Expand All @@ -256,7 +286,7 @@ def applied_to_request(self, request: Any = None) -> Generator[None, None, None]
request.auth = self.auth

try:
yield
yield request
finally:
if has_user:
request.user = old_user
Expand All @@ -272,6 +302,7 @@ def applied_to_request(self, request: Any = None) -> Generator[None, None, None]
class MiddlewareAuthenticationResponse(AuthenticationContext):
expired: bool = False
user_from_signed_request: bool = False
accessed: Set[str] = Field(default_factory=set)


class RpcAuthProviderFlags(RpcModel):
Expand Down
2 changes: 1 addition & 1 deletion tests/sentry/web/frontend/test_organization_avatar.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from sentry.web.frontend.generic import FOREVER_CACHE


@region_silo_test
@region_silo_test(stable=True)
class OrganizationAvatarTest(TestCase):
def test_headers(self):
org = self.create_organization()
Expand Down

0 comments on commit aa1d06b

Please sign in to comment.