def post(self, request, project): if not features.has(APP_STORE_CONNECT_FEATURE_NAME, project.organization, actor=request.user): return Response(status=404) serializer = AppStoreConnectCredentialsSerializer(data=request.data) if not serializer.is_valid(): return Response(serializer.errors, status=400) data = serializer.validated_data credentials = appstore_connect.AppConnectCredentials( key_id=data.get("appconnectKey"), key=data.get("appconnectPrivateKey"), issuer_id=data.get("appconnectIssuer"), ) session = requests.Session() apps = appstore_connect.get_apps(session, credentials) if apps is None: return Response("App connect authentication error.", status=401) apps = [{ "name": app.name, "bundleId": app.bundle_id, "appId": app.app_id } for app in apps] result = {"apps": apps} return Response(result, status=200)
def api_credentials( self, request) -> appstore_connect.AppConnectCredentials: # type: ignore """An App Store Connect API key in the form of AppConnectCredentials. If ``apikey.json`` is present in the current directory it will load the credentials from this json for the ``live`` param, the format should look like this: ```json { "key_id": "AAAAAAAAAA", "issuer_id": "abcdef01-2345-6789-abcd-ef0123456789", "private_key": "-----BEGIN PRIVATE KEY-----\na_bunch_of_\n_separated_base64\n-----END PRIVATE KEY-----\n" } ``` For the ``responses`` param a fake value is returned with a key_id="AAAAAAAAAA". """ if request.param == "live": here = pathlib.Path(__file__).parent keyfile = here / "apikey.json" try: data = json.loads(keyfile.read_text(encoding="utf-8")) except FileNotFoundError: pytest.skip("No API key available for live tests") else: return appstore_connect.AppConnectCredentials( key_id=data["key_id"], issuer_id=data["issuer_id"], key=data["private_key"]) else: # NOTE: This key has been generated outside of App Store Connect for the sole # purpose of being used in this test. The key is not associated with any account # on App Store Connect, and therefore is unable to access any live data. return appstore_connect.AppConnectCredentials( key_id="AAAAAAAAAA", issuer_id="12345678-abcd-abcd-abcd-1234567890ab", key=(textwrap.dedent(""" -----BEGIN EC PRIVATE KEY----- MHcCAQEEILd+RopXKDeu4wvj01ydqDp9goiI2KroiY4wgrMKz4j4oAoGCCqGSM49 AwEHoUQDQgAEe0GpznJGxz5cLukKBneiXlbPEEZRvqaKmpdd5Es+KQW0RK/9WmXK J9b/VBtFOSMiVav8iev+Kr/xPqcoor6Mpw== -----END EC PRIVATE KEY----- """)), )
def get(self, request, project, credentials_id): if not features.has(APP_STORE_CONNECT_FEATURE_NAME, project.organization, actor=request.user): return Response(status=404) symbol_source_cfg = get_app_store_config(project, credentials_id) key = project.get_option(CREDENTIALS_KEY_NAME) if key is None or symbol_source_cfg is None: return Response(status=404) if symbol_source_cfg.get("itunesCreated") is not None: expiration_date = (dateutil.parser.isoparse( symbol_source_cfg.get("itunesCreated")) + ITUNES_TOKEN_VALIDITY) else: expiration_date = None try: secrets = encrypt.decrypt_object( symbol_source_cfg.get("encrypted"), key) except ValueError: return Response(status=500) credentials = appstore_connect.AppConnectCredentials( key_id=symbol_source_cfg.get("appconnectKey"), key=secrets.get("appconnectPrivateKey"), issuer_id=symbol_source_cfg.get("appconnectIssuer"), ) session = requests.Session() apps = appstore_connect.get_apps(session, credentials) appstore_valid = apps is not None itunes_connect.load_session_cookie(session, secrets.get("itunesSession")) itunes_session_info = itunes_connect.get_session_info(session) itunes_session_valid = itunes_session_info is not None return Response( { "appstoreCredentialsValid": appstore_valid, "itunesSessionValid": itunes_session_valid, "itunesSessionRefreshAt": expiration_date if itunes_session_valid else None, }, status=200, )
def post(self, request: Request, project: Project) -> Response: serializer = AppStoreConnectCredentialsSerializer(data=request.data) if not serializer.is_valid(): return Response(serializer.errors, status=400) data = serializer.validated_data cfg_id: Optional[str] = data.get("id") apc_key: Optional[str] = data.get("appconnectKey") apc_private_key: Optional[str] = data.get("appconnectPrivateKey") apc_issuer: Optional[str] = data.get("appconnectIssuer") if cfg_id: try: current_config = appconnect.AppStoreConnectConfig.from_project_config( project, cfg_id) except KeyError: return Response(status=404) if not apc_key: apc_key = current_config.appconnectKey if not apc_private_key: apc_private_key = current_config.appconnectPrivateKey if not apc_issuer: apc_issuer = current_config.appconnectIssuer if not apc_key or not apc_private_key or not apc_issuer: return Response("Incomplete API credentials", status=400) credentials = appstore_connect.AppConnectCredentials( key_id=apc_key, key=apc_private_key, issuer_id=apc_issuer, ) session = requests.Session() try: apps = appstore_connect.get_apps(session, credentials) except appstore_connect.UnauthorizedError: raise AppConnectAuthenticationError except appstore_connect.ForbiddenError: raise AppConnectForbiddenError if apps is None: raise AppConnectAuthenticationError all_apps = [{ "name": app.name, "bundleId": app.bundle_id, "appId": app.app_id } for app in apps] result = {"apps": all_apps} return Response(result, status=200)
def from_config(cls, config: AppStoreConnectConfig) -> "AppConnectClient": """Creates a new client from an appStoreConnect symbol source config. This config is normally stored as a symbol source of type ``appStoreConnect`` in a project's ``sentry:symbol_sources`` property. """ api_credentials = appstore_connect.AppConnectCredentials( key_id=config.appconnectKey, key=config.appconnectPrivateKey, issuer_id=config.appconnectIssuer, ) return cls( api_credentials=api_credentials, app_id=config.appId, )
def get(self, request, project, credentials_id): if not features.has(app_store_connect_feature_name(), project.organization, actor=request.user): return Response(status=404) credentials = get_app_store_credentials(project, credentials_id) key = project.get_option(credentials_key_name()) if key is None or credentials is None: return Response(status=404) try: secrets = encrypt.decrypt_object(credentials.get("encrypted"), key) except ValueError: return Response(status=500) credentials = appstore_connect.AppConnectCredentials( key_id=credentials.get("appconnectKey"), key=secrets.get("appconnectPrivateKey"), issuer_id=credentials.get("appconnectIssuer"), ) session = requests.Session() apps = appstore_connect.get_apps(session, credentials) appstore_valid = apps is not None itunes_connect.load_session_cookie(session, secrets.get("itunesSession")) itunes_session_info = itunes_connect.get_session_info(session) itunes_session_valid = itunes_session_info is not None return Response( { "appstoreCredentialsValid": appstore_valid, "itunesSessionValid": itunes_session_valid, }, status=200, )
def get(self, request: Request, project: Project, credentials_id: str) -> Response: try: symbol_source_cfg = appconnect.AppStoreConnectConfig.from_project_config( project, credentials_id) except KeyError: return Response(status=404) credentials = appstore_connect.AppConnectCredentials( key_id=symbol_source_cfg.appconnectKey, key=symbol_source_cfg.appconnectPrivateKey, issuer_id=symbol_source_cfg.appconnectIssuer, ) session = requests.Session() apps = appstore_connect.get_apps(session, credentials) try: itunes_client = itunes_connect.ITunesClient.from_session_cookie( symbol_source_cfg.itunesSession) itunes_session_info = itunes_client.request_session_info() except itunes_connect.SessionExpiredError: itunes_session_info = None pending_downloads = AppConnectBuild.objects.filter( project=project, fetched=False).count() latest_build = (AppConnectBuild.objects.filter( project=project, bundle_id=symbol_source_cfg.bundleId).order_by( "-uploaded_to_appstore").first()) if latest_build is None: latestBuildVersion = None latestBuildNumber = None else: latestBuildVersion = latest_build.bundle_short_version latestBuildNumber = latest_build.bundle_version try: check_entry = LatestAppConnectBuildsCheck.objects.get( project=project, source_id=symbol_source_cfg.id) # If the source was only just created then it's possible that sentry hasn't checked for any # new builds for it yet. except LatestAppConnectBuildsCheck.DoesNotExist: last_checked_builds = None else: last_checked_builds = check_entry.last_checked return Response( { "appstoreCredentialsValid": apps is not None, "pendingDownloads": pending_downloads, "latestBuildVersion": latestBuildVersion, "latestBuildNumber": latestBuildNumber, "lastCheckedBuilds": last_checked_builds, "promptItunesSession": bool(pending_downloads and itunes_session_info is None), }, status=200, )
def get(self, request: Request, project: Project) -> Response: config_ids = appconnect.AppStoreConnectConfig.all_config_ids(project) statuses = {} for config_id in config_ids: try: symbol_source_cfg = appconnect.AppStoreConnectConfig.from_project_config( project, config_id) except KeyError: continue credentials = appstore_connect.AppConnectCredentials( key_id=symbol_source_cfg.appconnectKey, key=symbol_source_cfg.appconnectPrivateKey, issuer_id=symbol_source_cfg.appconnectIssuer, ) session = requests.Session() try: apps = appstore_connect.get_apps(session, credentials) except appstore_connect.UnauthorizedError: asc_credentials = { "status": "invalid", "code": AppConnectAuthenticationError.code, } except appstore_connect.ForbiddenError: asc_credentials = { "status": "invalid", "code": AppConnectForbiddenError.code } else: if apps: asc_credentials = {"status": "valid"} else: asc_credentials = { "status": "invalid", "code": AppConnectAuthenticationError.code, } # TODO: is it possible to set up two configs pointing to the same app? pending_downloads = AppConnectBuild.objects.filter( project=project, app_id=symbol_source_cfg.appId, fetched=False).count() latest_build = (AppConnectBuild.objects.filter( project=project, bundle_id=symbol_source_cfg.bundleId).order_by( "-uploaded_to_appstore").first()) if latest_build is None: latestBuildVersion = None latestBuildNumber = None else: latestBuildVersion = latest_build.bundle_short_version latestBuildNumber = latest_build.bundle_version try: check_entry = LatestAppConnectBuildsCheck.objects.get( project=project, source_id=symbol_source_cfg.id) # If the source was only just created then it's possible that sentry hasn't checked for any # new builds for it yet. except LatestAppConnectBuildsCheck.DoesNotExist: last_checked_builds = None else: last_checked_builds = check_entry.last_checked statuses[config_id] = { "credentials": asc_credentials, "pendingDownloads": pending_downloads, "latestBuildVersion": latestBuildVersion, "latestBuildNumber": latestBuildNumber, "lastCheckedBuilds": last_checked_builds, } return Response(statuses, status=200)