Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion src/sentry/objectstore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,23 @@
from objectstore_client import (
Client,
MetricsBackend,
Permission,
Session,
TimeToLive,
TokenGenerator,
Usecase,
parse_accept_encoding,
)
from objectstore_client.metrics import Tags
from objectstore_client.scope import Scope

from sentry import options
from sentry.utils import metrics as sentry_metrics
from sentry.utils.env import in_test_environment

__all__ = ["get_attachments_session", "parse_accept_encoding"]
__all__ = ["get_attachments_session", "mint_read_token", "parse_accept_encoding"]

_READ_ONLY_TOKEN_EXPIRY_SECONDS = 300 # 5 minutes


def default_attachment_retention() -> int:
Expand Down Expand Up @@ -119,6 +123,45 @@ def get_preprod_session(org: int, project: int) -> Session:
return get_client().session(_PREPROD_USECASE, org=org, project=project)


_READ_ONLY_TOKEN_GENERATOR: TokenGenerator | None = None


def _get_read_only_token_generator() -> TokenGenerator | None:
"""
Returns a TokenGenerator configured with read-only permissions and a short
expiry, suitable for minting tokens to pass to the frontend.
"""
global _READ_ONLY_TOKEN_GENERATOR
if _READ_ONLY_TOKEN_GENERATOR is None:
from sentry import options as options_store

os_options = options_store.get("objectstore.config")
signing_key_options = os_options.get("token_generator")
if (
signing_key_options
and signing_key_options.get("kid")
and signing_key_options.get("secret_key")
):
_READ_ONLY_TOKEN_GENERATOR = TokenGenerator(
kid=signing_key_options["kid"],
secret_key=signing_key_options["secret_key"],
expiry_seconds=_READ_ONLY_TOKEN_EXPIRY_SECONDS,
permissions=[Permission.OBJECT_READ],
)
return _READ_ONLY_TOKEN_GENERATOR


def mint_read_token(usecase: str, **scopes: str | int | bool) -> str | None:
"""
Mint a short-lived, read-only token for the given usecase and scope.
Returns None if token generation is not configured.
"""
generator = _get_read_only_token_generator()
if generator is None:
return None
return generator.sign_for_scope(usecase, Scope(**scopes))


_IS_SYMBOLICATOR_CONTAINER: bool | None = None


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from sentry.models.commitcomparison import CommitComparison
from sentry.models.organization import Organization
from sentry.models.project import Project
from sentry.objectstore import get_preprod_session
from sentry.objectstore import get_preprod_session, mint_read_token
from sentry.preprod.analytics import (
PreprodArtifactApiDeleteEvent,
PreprodArtifactApiGetSnapshotDetailsEvent,
Expand Down Expand Up @@ -405,6 +405,8 @@ def get(self, request: Request, organization: Organization, snapshot_id: str) ->
approvers=[],
)

token = mint_read_token("preprod", org=organization.id, project=artifact.project_id)

return Response(
SnapshotDetailsApiResponse(
head_artifact_id=str(artifact.id),
Expand All @@ -429,6 +431,7 @@ def get(self, request: Request, organization: Organization, snapshot_id: str) ->
errored_count=len(categorized.errored),
comparison_run_info=run_info,
approval_info=approval_info,
objectstore_token=token,
).dict()
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import cell_silo_endpoint
from sentry.models.project import Project
from sentry.objectstore import mint_read_token
from sentry.preprod.analytics import PreprodArtifactApiGetBuildDetailsEvent
from sentry.preprod.api.bases.preprod_artifact_endpoint import PreprodArtifactEndpoint
from sentry.preprod.api.models.project_preprod_build_details_models import (
Expand Down Expand Up @@ -70,5 +71,8 @@ def get(
if head_artifact.state == PreprodArtifact.ArtifactState.FAILED:
return Response({"error": head_artifact.error_message}, status=400)
else:
build_details = transform_preprod_artifact_to_build_details(head_artifact)
token = mint_read_token("preprod", org=project.organization_id, project=project.id)
build_details = transform_preprod_artifact_to_build_details(
head_artifact, objectstore_token=token
)
return Response(build_details.dict())
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ class BuildDetailsApiResponse(BaseModel):
posted_status_checks: PostedStatusChecks | None = None
base_artifact_id: str | None = None
base_build_info: BuildDetailsAppInfo | None = None
objectstore_token: str | None = None


def create_build_details_app_info(artifact: PreprodArtifact) -> BuildDetailsAppInfo:
Expand Down Expand Up @@ -264,6 +265,7 @@ def to_size_info(

def transform_preprod_artifact_to_build_details(
artifact: PreprodArtifact,
objectstore_token: str | None = None,
) -> BuildDetailsApiResponse:
size_metrics_list = list(artifact.preprodartifactsizemetrics_set.all())

Expand Down Expand Up @@ -327,6 +329,7 @@ def transform_preprod_artifact_to_build_details(
posted_status_checks=posted_status_checks,
base_artifact_id=base_artifact.id if base_artifact else None,
base_build_info=base_build_info,
objectstore_token=objectstore_token,
)


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,7 @@ class SnapshotDetailsApiResponse(BaseModel):

approval_info: SnapshotApprovalInfo | None = None

objectstore_token: str | None = None


# TODO: POST request in the future when we migrate away from current schemas
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export function BuildCompareHeaderContent(props: BuildCompareHeaderContentProps)
appName={buildDetails.app_info.name}
appIconId={buildDetails.app_info.app_icon_id}
projectId={buildDetails.project_id}
objectstoreToken={buildDetails.objectstore_token}
/>
<Text>{buildDetails.app_info.name}</Text>
</Flex>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ interface BuildDetailsSidebarAppInfoProps {
artifactId: string;
projectId: number | null;
projectSlug: string | null;
objectstoreToken?: string | null;
}

export function BuildDetailsSidebarAppInfo(props: BuildDetailsSidebarAppInfoProps) {
Expand All @@ -44,6 +45,7 @@ export function BuildDetailsSidebarAppInfo(props: BuildDetailsSidebarAppInfoProp
appName={props.appInfo.name}
appIconId={props.appInfo.app_icon_id}
projectId={props.projectId}
objectstoreToken={props.objectstoreToken}
/>
<Heading as="h3">{props.appInfo.name}</Heading>
</Fragment>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export function BuildDetailsSidebarContent(props: BuildDetailsSidebarContentProp
projectId={buildDetailsData.project_id}
projectSlug={buildDetailsData.project_slug}
artifactId={artifactId}
objectstoreToken={buildDetailsData.objectstore_token}
/>

{/* Status check info */}
Expand Down
8 changes: 6 additions & 2 deletions static/app/views/preprod/components/appIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,20 @@ import {useOrganization} from 'sentry/utils/useOrganization';
interface AppIconProps {
appName: string;
appIconId?: string | null;
objectstoreToken?: string | null;
projectId?: number | null;
}

export function AppIcon({appName, appIconId, projectId}: AppIconProps) {
export function AppIcon({appName, appIconId, projectId, objectstoreToken}: AppIconProps) {
const organization = useOrganization();
const [imageError, setImageError] = useState(false);

let iconUrl = undefined;
if (appIconId && projectId) {
iconUrl = `/api/0/organizations/${organization.slug}/objectstore/v1/objects/preprod/org=${organization.id};project=${projectId}/${organization.id}/${projectId}/${appIconId}`;
const authSuffix = objectstoreToken
? `?X-Os-Auth=${encodeURIComponent(objectstoreToken)}`
: '';
iconUrl = `/api/0/organizations/${organization.slug}/objectstore/v1/objects/preprod/org=${organization.id};project=${projectId}/${organization.id}/${projectId}/${appIconId}${authSuffix}`;
}

return (
Expand Down
1 change: 1 addition & 0 deletions static/app/views/preprod/install/buildInstallHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export function BuildInstallHeader(props: BuildInstallHeaderProps) {
appName={appInfo.name}
appIconId={appInfo.app_icon_id}
projectId={buildDetailsData.project_id}
objectstoreToken={buildDetailsData.objectstore_token}
/>
) : null}
{appInfo.name ? <span>{appInfo.name}</span> : null}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
export type DiffMode = 'split' | 'wipe' | 'onion';

interface DiffImageDisplayProps {
authSuffix: string;
diffImageBaseUrl: string;
diffMode: DiffMode;
imageBaseUrl: string;
Expand All @@ -37,6 +38,7 @@ export function DiffImageDisplay({
pair,
imageBaseUrl,
diffImageBaseUrl,
authSuffix,
showOverlay,
overlayColor,
diffMode,
Expand All @@ -46,10 +48,10 @@ export function DiffImageDisplay({
const [onionOpacity, setOnionOpacity] = useState(50);
const blobUrlRef = useRef<string | null>(null);

const baseImageUrl = `${imageBaseUrl}${pair.base_image.key}`;
const headImageUrl = `${imageBaseUrl}${pair.head_image.key}`;
const baseImageUrl = `${imageBaseUrl}${pair.base_image.key}${authSuffix}`;
const headImageUrl = `${imageBaseUrl}${pair.head_image.key}${authSuffix}`;
const diffImageUrl = pair.diff_image_key
? `${diffImageBaseUrl}${pair.diff_image_key}`
? `${diffImageBaseUrl}${pair.diff_image_key}${authSuffix}`
: null;

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {DiffImageDisplay, type DiffMode} from './imageDisplay/diffImageDisplay';
import {SingleImageDisplay} from './imageDisplay/singleImageDisplay';

interface SnapshotMainContentProps {
authSuffix: string;
diffImageBaseUrl: string;
diffMode: DiffMode;
imageBaseUrl: string;
Expand All @@ -37,6 +38,7 @@ export function SnapshotMainContent({
onVariantChange,
imageBaseUrl,
diffImageBaseUrl,
authSuffix,
showOverlay,
onShowOverlayChange,
overlayColor,
Expand Down Expand Up @@ -99,6 +101,7 @@ export function SnapshotMainContent({
pair={currentPair}
imageBaseUrl={imageBaseUrl}
diffImageBaseUrl={diffImageBaseUrl}
authSuffix={authSuffix}
showOverlay={showOverlay}
overlayColor={overlayColor}
diffMode={diffMode}
Expand All @@ -115,7 +118,7 @@ export function SnapshotMainContent({
}
const displayName = getImageName(currentImage);
const totalVariants = selectedItem.images.length;
const imageUrl = `${imageBaseUrl}${currentImage.key}`;
const imageUrl = `${imageBaseUrl}${currentImage.key}${authSuffix}`;

return (
<Flex direction="column" gap="0" padding="0" height="100%" width="100%">
Expand Down Expand Up @@ -150,7 +153,7 @@ export function SnapshotMainContent({
return null;
}
const totalVariants = selectedItem.pairs.length;
const imageUrl = `${imageBaseUrl}${currentPair.head_image.key}`;
const imageUrl = `${imageBaseUrl}${currentPair.head_image.key}${authSuffix}`;
const displayName = getImageName(currentPair.head_image);

return (
Expand Down Expand Up @@ -199,7 +202,7 @@ export function SnapshotMainContent({
return null;
}
const displayName = getImageName(currentImage);
const imageUrl = `${imageBaseUrl}${currentImage.key}`;
const imageUrl = `${imageBaseUrl}${currentImage.key}${authSuffix}`;
const totalVariants = selectedItem.images.length;
const STATUS_LABELS: Record<string, string> = {
added: t('Added'),
Expand Down
4 changes: 4 additions & 0 deletions static/app/views/preprod/snapshots/snapshots.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,9 @@ export default function SnapshotsPage() {
const objectstoreBaseUrl = data
? `/api/0/organizations/${organization.slug}/objectstore/v1/objects/preprod/org=${organization.id};project=${data.project_id}/${organization.id}/${data.project_id}/`
: '';
const authSuffix = data?.objectstore_token
? `?X-Os-Auth=${encodeURIComponent(data.objectstore_token)}`
: '';
const imageBaseUrl = objectstoreBaseUrl;
const diffImageBaseUrl = objectstoreBaseUrl;

Expand Down Expand Up @@ -357,6 +360,7 @@ export default function SnapshotsPage() {
onVariantChange={setVariantIndex}
imageBaseUrl={imageBaseUrl}
diffImageBaseUrl={diffImageBaseUrl}
authSuffix={authSuffix}
showOverlay={showOverlay}
onShowOverlayChange={setShowOverlay}
overlayColor={overlayColor}
Expand Down
1 change: 1 addition & 0 deletions static/app/views/preprod/types/buildDetailsTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface BuildDetailsApiResponse {
posted_status_checks?: PostedStatusChecks | null;
base_artifact_id?: string | null;
base_build_info?: BuildDetailsAppInfo | null;
objectstore_token?: string | null;
}

interface BuildDetailsDistributionInfo {
Expand Down
2 changes: 2 additions & 0 deletions static/app/views/preprod/types/snapshotTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ export interface SnapshotDetailsApiResponse {

approval_info?: SnapshotApprovalInfo | null;

objectstore_token?: string | null;

// Diff fields
added: SnapshotImage[];
added_count: number;
Expand Down
1 change: 1 addition & 0 deletions tests/sentry/preprod/api/endpoints/test_builds.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ def test_one_build(self) -> None:
"posted_status_checks": None,
"project_slug": "bar",
"size_info": None,
"objectstore_token": None,
}
]

Expand Down
Loading