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)
예제 #2
0
    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,
        )
예제 #4
0
    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)
예제 #5
0
    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,
        )
예제 #7
0
    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,
        )
예제 #8
0
    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)