Skip to content

Commit bcfd90a

Browse files
authored
fix(appstore-connect): Add a new endpoint that returns the status of all configured apps (#29728)
1 parent 161459f commit bcfd90a

File tree

13 files changed

+239
-79
lines changed

13 files changed

+239
-79
lines changed

src/sentry/api/endpoints/project_app_store_connect_credentials.py

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@
3939

4040
from sentry import features
4141
from sentry.api.bases.project import ProjectEndpoint, ProjectPermission, StrictProjectPermission
42-
from sentry.api.exceptions import AppConnectAuthenticationError, AppConnectMultipleSourcesError
42+
from sentry.api.exceptions import (
43+
AppConnectAuthenticationError,
44+
AppConnectForbiddenError,
45+
AppConnectMultipleSourcesError,
46+
)
4347
from sentry.api.fields.secret import SecretField, validate_secret
4448
from sentry.lang.native import appconnect
4549
from sentry.lang.native.symbolicator import redact_source_secrets, secret_fields
@@ -411,3 +415,119 @@ def get(self, request: Request, project: Project, credentials_id: str) -> Respon
411415
},
412416
status=200,
413417
)
418+
419+
420+
class AppStoreConnectStatusEndpoint(ProjectEndpoint): # type: ignore
421+
"""Returns a summary of the project's App Store Connect configuration
422+
and builds.
423+
424+
``GET projects/{org_slug}/{proj_slug}/appstoreconnect/status/``
425+
426+
Response:
427+
```json
428+
{
429+
"abc123": {
430+
"credentials": { status: "valid" } | { status: "invalid", code: "app-connect-forbidden-error" },
431+
"pendingDownloads": 123,
432+
"latestBuildVersion: "9.8.7" | null,
433+
"latestBuildNumber": "987000" | null,
434+
"lastCheckedBuilds": "YYYY-MM-DDTHH:MM:SS.SSSSSSZ" | null
435+
},
436+
"...": {
437+
"credentials": ...,
438+
"pendingDownloads": ...,
439+
"latestBuildVersion: ...,
440+
"latestBuildNumber": ...,
441+
"lastCheckedBuilds": ...
442+
},
443+
...
444+
}
445+
```
446+
447+
* ``pendingDownloads`` is the number of pending build dSYM downloads.
448+
449+
* ``latestBuildVersion`` and ``latestBuildNumber`` together form a unique identifier for
450+
the latest build recognized by Sentry.
451+
452+
* ``lastCheckedBuilds`` is when sentry last checked for new builds, regardless
453+
of whether there were any or no builds in App Store Connect at the time.
454+
"""
455+
456+
permission_classes = [ProjectPermission]
457+
458+
def get(self, request: Request, project: Project) -> Response:
459+
config_ids = appconnect.AppStoreConnectConfig.all_config_ids(project)
460+
statuses = {}
461+
for config_id in config_ids:
462+
try:
463+
symbol_source_cfg = appconnect.AppStoreConnectConfig.from_project_config(
464+
project, config_id
465+
)
466+
except KeyError:
467+
continue
468+
469+
credentials = appstore_connect.AppConnectCredentials(
470+
key_id=symbol_source_cfg.appconnectKey,
471+
key=symbol_source_cfg.appconnectPrivateKey,
472+
issuer_id=symbol_source_cfg.appconnectIssuer,
473+
)
474+
475+
session = requests.Session()
476+
477+
try:
478+
apps = appstore_connect.get_apps(session, credentials)
479+
except appstore_connect.UnauthorizedError:
480+
asc_credentials = {
481+
"status": "invalid",
482+
"code": AppConnectAuthenticationError.code,
483+
}
484+
except appstore_connect.ForbiddenError:
485+
asc_credentials = {"status": "invalid", "code": AppConnectForbiddenError.code}
486+
else:
487+
if apps:
488+
asc_credentials = {"status": "valid"}
489+
else:
490+
asc_credentials = {
491+
"status": "invalid",
492+
"code": AppConnectAuthenticationError.code,
493+
}
494+
495+
# TODO: is it possible to set up two configs pointing to the same app?
496+
pending_downloads = AppConnectBuild.objects.filter(
497+
project=project, app_id=symbol_source_cfg.appId, fetched=False
498+
).count()
499+
500+
latest_build = (
501+
AppConnectBuild.objects.filter(
502+
project=project, bundle_id=symbol_source_cfg.bundleId
503+
)
504+
.order_by("-uploaded_to_appstore")
505+
.first()
506+
)
507+
if latest_build is None:
508+
latestBuildVersion = None
509+
latestBuildNumber = None
510+
else:
511+
latestBuildVersion = latest_build.bundle_short_version
512+
latestBuildNumber = latest_build.bundle_version
513+
514+
try:
515+
check_entry = LatestAppConnectBuildsCheck.objects.get(
516+
project=project, source_id=symbol_source_cfg.id
517+
)
518+
# If the source was only just created then it's possible that sentry hasn't checked for any
519+
# new builds for it yet.
520+
except LatestAppConnectBuildsCheck.DoesNotExist:
521+
last_checked_builds = None
522+
else:
523+
last_checked_builds = check_entry.last_checked
524+
525+
statuses[config_id] = {
526+
"credentials": asc_credentials,
527+
"pendingDownloads": pending_downloads,
528+
"latestBuildVersion": latestBuildVersion,
529+
"latestBuildNumber": latestBuildNumber,
530+
"lastCheckedBuilds": last_checked_builds,
531+
}
532+
533+
return Response(statuses, status=200)

src/sentry/api/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ class TwoFactorRequired(SentryAPIException):
9797
message = "Organization requires two-factor authentication to be enabled"
9898

9999

100+
class AppConnectForbiddenError(SentryAPIException):
101+
status_code = status.HTTP_403_FORBIDDEN
102+
code = "app-connect-forbidden-error"
103+
message = "App connect Forbidden error"
104+
105+
100106
class AppConnectAuthenticationError(SentryAPIException):
101107
status_code = status.HTTP_401_UNAUTHORIZED
102108
code = "app-connect-authentication-error"

src/sentry/api/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@
282282
AppStoreConnectAppsEndpoint,
283283
AppStoreConnectCreateCredentialsEndpoint,
284284
AppStoreConnectCredentialsValidateEndpoint,
285+
AppStoreConnectStatusEndpoint,
285286
AppStoreConnectUpdateCredentialsEndpoint,
286287
)
287288
from .endpoints.project_avatar import ProjectAvatarEndpoint
@@ -2027,6 +2028,11 @@
20272028
AppStoreConnectCredentialsValidateEndpoint.as_view(),
20282029
name="sentry-api-0-project-appstoreconnect-validate",
20292030
),
2031+
url(
2032+
r"^(?P<organization_slug>[^\/]+)/(?P<project_slug>[^\/]+)/appstoreconnect/status/$",
2033+
AppStoreConnectStatusEndpoint.as_view(),
2034+
name="sentry-api-0-project-appstoreconnect-status",
2035+
),
20302036
url(
20312037
r"^(?P<organization_slug>[^\/]+)/(?P<project_slug>[^\/]+)/appstoreconnect/(?P<credentials_id>[^\/]+)/$",
20322038
AppStoreConnectUpdateCredentialsEndpoint.as_view(),

src/sentry/utils/appleconnect/appstore_connect.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class UnauthorizedError(RequestError):
3636

3737

3838
class ForbiddenError(RequestError):
39-
"""The App Store Connect session does not have access to the requested dSYM."""
39+
"""Forbidden: authentication token does not have sufficient permissions."""
4040

4141
pass
4242

@@ -154,6 +154,8 @@ def _get_appstore_json(
154154

155155
if response.status_code == HTTPStatus.UNAUTHORIZED:
156156
raise UnauthorizedError(full_url)
157+
elif response.status_code == HTTPStatus.FORBIDDEN:
158+
raise ForbiddenError(full_url)
157159
else:
158160
raise RequestError(full_url)
159161
try:

static/app/components/globalAppStoreConnectUpdateAlert/updateAlert.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {IconClose, IconRefresh} from 'app/icons';
1111
import {t} from 'app/locale';
1212
import space from 'app/styles/space';
1313
import {Organization, Project} from 'app/types';
14-
import {AppStoreConnectValidationData} from 'app/types/debugFiles';
14+
import {AppStoreConnectStatusData} from 'app/types/debugFiles';
1515
import {promptIsDismissed} from 'app/utils/promptIsDismissed';
1616
import withApi from 'app/utils/withApi';
1717

@@ -69,14 +69,14 @@ function UpdateAlert({api, Wrapper, isCompact, project, organization, className}
6969
}
7070

7171
function renderMessage(
72-
appStoreConnectValidationData: AppStoreConnectValidationData,
72+
appStoreConnectStatusData: AppStoreConnectStatusData,
7373
projectSettingsLink: string
7474
) {
75-
if (!appStoreConnectValidationData.updateAlertMessage) {
75+
if (!appStoreConnectStatusData.updateAlertMessage) {
7676
return null;
7777
}
7878

79-
const {updateAlertMessage} = appStoreConnectValidationData;
79+
const {updateAlertMessage} = appStoreConnectStatusData;
8080

8181
return (
8282
<div>

static/app/components/modals/debugFileCustomRepository/appStoreConnect/index.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import Button from 'app/components/button';
99
import ButtonBar from 'app/components/buttonBar';
1010
import LoadingIndicator from 'app/components/loadingIndicator';
1111
import {AppStoreConnectContextProps} from 'app/components/projects/appStoreConnectContext';
12-
import {appStoreConnectAlertMessage} from 'app/components/projects/appStoreConnectContext/utils';
1312
import {IconWarning} from 'app/icons';
1413
import {t, tct} from 'app/locale';
1514
import space from 'app/styles/space';
@@ -57,7 +56,7 @@ function AppStoreConnect({
5756
onSubmit,
5857
appStoreConnectContext,
5958
}: Props) {
60-
const {updateAlertMessage} = appStoreConnectContext ?? {};
59+
const {credentials} = appStoreConnectContext ?? {};
6160

6261
const [isLoading, setIsLoading] = useState(false);
6362
const [activeStep, setActiveStep] = useState(0);
@@ -114,6 +113,7 @@ function AppStoreConnect({
114113
const appStoreConnnectError = getAppStoreErrorMessage(error);
115114
if (typeof appStoreConnnectError === 'string') {
116115
// app-connect-authentication-error
116+
// app-connect-forbidden-error
117117
addErrorMessage(appStoreConnnectError);
118118
return;
119119
}
@@ -238,13 +238,16 @@ function AppStoreConnect({
238238
if (activeStep !== 0) {
239239
return alerts;
240240
}
241-
242-
if (updateAlertMessage === appStoreConnectAlertMessage.appStoreCredentialsInvalid) {
241+
if (credentials?.status === 'invalid') {
243242
alerts.push(
244243
<StyledAlert type="warning" icon={<IconWarning />}>
245-
{t(
246-
'Your App Store Connect credentials are invalid. To reconnect, update your credentials.'
247-
)}
244+
{credentials.code === 'app-connect-forbidden-error'
245+
? t(
246+
'Your App Store Connect credentials have insufficient permissions. To reconnect, update your credentials.'
247+
)
248+
: t(
249+
'Your App Store Connect credentials are invalid. To reconnect, update your credentials.'
250+
)}
248251
</StyledAlert>
249252
);
250253
}

static/app/components/modals/debugFileCustomRepository/appStoreConnect/utils.tsx

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,17 @@ const fieldErrorMessageMapping = {
2828
},
2929
};
3030

31-
type ErrorCodeDetailed =
31+
export type ErrorCodeDetailed =
3232
| 'app-connect-authentication-error'
33+
| 'app-connect-forbidden-error'
3334
| 'app-connect-multiple-sources-error';
3435

36+
export type ValidationErrorDetailed = {
37+
code: ErrorCodeDetailed;
38+
};
39+
3540
type ResponseJSONDetailed = {
36-
detail: {
37-
code: ErrorCodeDetailed;
41+
detail: ValidationErrorDetailed & {
3842
extra: Record<string, any>;
3943
message: string;
4044
};
@@ -64,21 +68,7 @@ export function getAppStoreErrorMessage(
6468
?.detail;
6569

6670
if (detailedErrorResponse) {
67-
switch (detailedErrorResponse.code) {
68-
case 'app-connect-authentication-error':
69-
return t(
70-
'We could not establish a connection with App Store Connect. Please check the entered App Store Connect credentials'
71-
);
72-
case 'app-connect-multiple-sources-error':
73-
return t(
74-
'Only one Apple App Store Connect application is allowed in this project'
75-
);
76-
default: {
77-
// this shall not happen
78-
Sentry.captureException(new Error('Unknown app store connect error'));
79-
return unexpectedErrorMessage;
80-
}
81-
}
71+
return getAppStoreValidationErrorMessage(detailedErrorResponse);
8272
}
8373

8474
const errorResponse = error.responseJSON as undefined | ResponseJSON;
@@ -116,3 +106,23 @@ export function getAppStoreErrorMessage(
116106
{}
117107
) as Record<keyof StepOneData, string>;
118108
}
109+
110+
export function getAppStoreValidationErrorMessage(
111+
error: ValidationErrorDetailed
112+
): string {
113+
switch (error.code) {
114+
case 'app-connect-authentication-error':
115+
return t(
116+
'Credentials are invalid or missing. Check the entered App Store Connect credentials are correct.'
117+
);
118+
case 'app-connect-forbidden-error':
119+
return t('The supplied API key does not have sufficient permissions.');
120+
case 'app-connect-multiple-sources-error':
121+
return t('Only one Apple App Store Connect application is allowed in this project');
122+
default: {
123+
// this shall not happen
124+
Sentry.captureException(new Error('Unknown app store connect error'));
125+
return unexpectedErrorMessage;
126+
}
127+
}
128+
}

0 commit comments

Comments
 (0)