Skip to content
This repository was archived by the owner on Aug 30, 2023. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
52ea1b2
Add itunesconnect basic plugin with client
HazAT Feb 20, 2017
400cffe
Add test config functionality
HazAT Feb 21, 2017
7c6fe1e
Cache itc response
HazAT Feb 21, 2017
de2d0b3
Add task functionality
HazAT Feb 21, 2017
de7215d
Add task
HazAT Feb 22, 2017
adb6ad3
Add app and dsym models, Update task to create models
HazAT Feb 22, 2017
7bea755
Download after build fetching, Make dsymfile unique
HazAT Feb 23, 2017
0ba90cd
Change unique key, Sync all dsym files
HazAT Feb 23, 2017
658e4a2
Update model, Use SafeSession, Check empty url
HazAT Feb 23, 2017
4b8caaa
Delete itc client cache on exception, Pass project to is enabled, Del…
HazAT Feb 24, 2017
4a68283
Add reset client methods
HazAT Feb 27, 2017
513b4a8
Add two_factor auth, Remove models and migration
HazAT Feb 28, 2017
bb7407e
Add 2FA request, Add Client model
HazAT Mar 1, 2017
03f2557
Reset client when settings are changed
HazAT Mar 2, 2017
09156d5
Add 2fa and test config for ITC
HazAT Mar 13, 2017
c87e9f6
Merge branch 'master' into feature/itunesconnect
HazAT Mar 13, 2017
bd87248
Use cached client for sync, Rename apps_to_sync to teams
HazAT Mar 16, 2017
fc2b16a
Fix syncing of apps and builds
HazAT Mar 23, 2017
0889695
Add test class for itunesconnect
HazAT Mar 23, 2017
08126ea
Update plugin to use new fields
HazAT Mar 23, 2017
f36f13f
Make 2FA more robust
HazAT Mar 24, 2017
7000e33
Add first_login_attempt, Split up login functions
HazAT Mar 24, 2017
a356cf0
Merge branch 'master' into feature/itunesconnect
HazAT Apr 5, 2017
ccc94c3
Add numberconfirm jsx
HazAT Apr 27, 2017
17d7b89
Merge branch 'master' into feature/itunesconnect
HazAT Apr 27, 2017
d104b78
Fix 2fa detection
HazAT Apr 27, 2017
0463743
Use olympus api of itunes connect
HazAT Jun 12, 2017
80aba12
Merge branch 'master' into feature/itunesconnect
HazAT Jun 12, 2017
6274a8d
Merge branch 'master' into feature/itunesconnect
HazAT Jun 28, 2017
74d7cb5
Remove 2fa, Cleanup code
HazAT Jun 29, 2017
06336f5
Auto sync account when page loads
HazAT Jul 3, 2017
d76e576
Remove 2fa urls, Add plugin enabled check
HazAT Jul 3, 2017
252062c
Merge branch 'master' into feature/itunesconnect
HazAT Jul 26, 2017
554ada2
Merge branch 'master' into feature/itunesconnect
HazAT Aug 9, 2017
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
3 changes: 3 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def pytest_configure(config):
'sentry_plugins.gitlab', 'sentry_plugins.pagerduty', 'sentry_plugins.pivotal',
'sentry_plugins.pushover', 'sentry_plugins.jira', 'sentry_plugins.segment',
'sentry_plugins.sessionstack', 'sentry_plugins.slack', 'sentry_plugins.victorops',
'sentry_plugins.itunesconnect'
)

# TODO(dcramer): we need a PluginAPITestCase that can do register/unregister
Expand All @@ -31,6 +32,7 @@ def pytest_configure(config):
from sentry_plugins.gitlab.plugin import GitLabPlugin
from sentry_plugins.heroku.plugin import HerokuPlugin
from sentry_plugins.hipchat_ac.plugin import HipchatPlugin
from sentry_plugins.itunesconnect.plugin import ItunesConnectPlugin
from sentry_plugins.jira.plugin import JiraPlugin
from sentry_plugins.pagerduty.plugin import PagerDutyPlugin
from sentry_plugins.pivotal.plugin import PivotalPlugin
Expand All @@ -46,6 +48,7 @@ def pytest_configure(config):
plugins.register(GitLabPlugin)
plugins.register(HerokuPlugin)
plugins.register(HipchatPlugin)
plugins.register(ItunesConnectPlugin)
plugins.register(JiraPlugin)
plugins.register(PagerDutyPlugin)
plugins.register(PivotalPlugin)
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ def run(self):
'gitlab = sentry_plugins.gitlab',
'heroku = sentry_plugins.heroku',
'hipchat_ac = sentry_plugins.hipchat_ac',
'itunesconnect = sentry_plugins.itunesconnect',
'jira = sentry_plugins.jira',
'jira_ac = sentry_plugins.jira_ac',
'pagerduty = sentry_plugins.pagerduty',
Expand All @@ -123,6 +124,7 @@ def run(self):
'gitlab = sentry_plugins.gitlab.plugin:GitLabPlugin',
'heroku = sentry_plugins.heroku.plugin:HerokuPlugin',
'hipchat_ac = sentry_plugins.hipchat_ac.plugin:HipchatPlugin',
'itunesconnect = sentry_plugins.itunesconnect.plugin:ItunesConnectPlugin',
'jira = sentry_plugins.jira.plugin:JiraPlugin',
'jira_ac = sentry_plugins.jira_ac.plugin:JiraACPlugin',
'pagerduty = sentry_plugins.pagerduty.plugin:PagerDutyPlugin',
Expand Down
1 change: 1 addition & 0 deletions src/sentry_plugins/itunesconnect/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from __future__ import absolute_import
228 changes: 228 additions & 0 deletions src/sentry_plugins/itunesconnect/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
from __future__ import absolute_import

import re

from six.moves.urllib.parse import urljoin
from requests.utils import dict_from_cookiejar, add_dict_to_cookiejar
from sentry.http import SafeSession

BASE_URL = 'https://itunesconnect.apple.com/'
API_BASE = urljoin(BASE_URL, 'WebObjects/iTunesConnect.woa/')
ISK_JS_URL = urljoin(BASE_URL, 'itc/static-resources/controllers/login_cntrl.js')
USER_DETAILS_URL = urljoin(API_BASE, 'ra/user/detail')

APPLEID_BASE_URL = 'https://idmsa.apple.com/'
LOGIN_URL = urljoin(APPLEID_BASE_URL, 'appleauth/auth/signin')

_isk_re = re.compile(r'itcServiceKey\s+=\s+["\'](.*)["\']')


class ItcError(Exception):
pass


class ItunesConnectClient(object):

def __init__(self):
self._reset()

def _reset(self):
self._session = SafeSession()
self._service_key = None
self._user_details = None
self._scnt = None
self._session_id = None
self.authenticated = False

def logout(self):
self._reset()

def login(self, email=None, password=None):
if email is None or password is None:
return

if self.authenticated:
# we don't want to login again
return

self._get_service_key()

rv = self._session.post(LOGIN_URL, json={
'accountName': email,
'password': password,
'rememberMe': False,
}, headers={
'X-Requested-With': 'XMLHttpRequest',
'X-Apple-Widget-Key': self._service_key
})

self._session_id = rv.headers.get('X-Apple-Id-Session-Id', None)
self._scnt = rv.headers.get('scnt', None)

# This is necessary because it sets some further cookies
rv = self._session.get(urljoin(API_BASE, 'wa'))
rv.raise_for_status()

# If we reach this we are authenticated
self.authenticated = True

def _get_service_key(self):
if self._service_key is not None:
return self._service_key
rv = self._session.get(ISK_JS_URL)
match = _isk_re.search(rv.text)
if match is not None:
self._service_key = match.group(1)
return self._service_key
raise ItcError('Could not find service key')

def _request_headers(self):
return {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Apple-Widget-Key': self._service_key,
'X-Apple-Id-Session-Id': self._session_id,
'scnt': self._scnt
}

@classmethod
def from_json(cls, data):
"""Creates an Itc object from json."""
rv = cls()

val = data.get('service_key')
if val is not None:
rv._service_key = val

val = data.get('session_id')
if val is not None:
rv._session_id = val

val = data.get('scnt')
if val is not None:
rv._scnt = val

val = data.get('authenticated')
if val is not None:
rv.authenticated = val

val = data.get('cookies')
if val:
add_dict_to_cookiejar(rv._session.cookies, val)

return rv

def to_json(self, ensure_user_details=True):
"""Converts an ITC into a JSON object for caching."""
return {
'service_key': self._service_key,
'scnt': self._scnt,
'session_id': self._session_id,
'authenticated': self.authenticated,
'cookies': dict_from_cookiejar(self._session.cookies),
}

def get_user_details(self):
"""Returns the user details. If they were not loaded yet this
triggers a refresh.
"""
if self._user_details is None:
self.refresh_user_details()
return self._user_details

def refresh_user_details(self):
"""Refreshes the user details."""
rv = self._session.get(USER_DETAILS_URL, headers=self._request_headers())
rv.raise_for_status()
data = rv.json()['data']

self._user_details = {
'apps': self._list_apps(),
'session': {
'ds_id': data['sessionToken']['dsId']
},
'email': data['userName'],
'name': data['displayName'],
'user_id': data['userId'],
}

def iter_apps(self):
"""Iterates over all apps the user has access to."""
return self.get_user_details()['apps']

def iter_app_builds(self, app):
"""Given an app ID, this iterates over all the builds that exist
for it.
"""
for platform in app['platforms']:
rv = self._session.get(urljoin(
API_BASE, 'ra/apps/%s/buildHistory?platform=%s' % (
app['id'], platform)))

rv.raise_for_status()

trains = rv.json()['data']['trains']
for train in trains:
if train.get('items') is None:
app_builds = self._fetch_build_history_with_train(app, train.get('versionString'), platform)
for app_build in app_builds:
yield {
'app_id': app['id'],
'platform': app_build['platform'],
'version': train['versionString'],
'build_id': app_build['buildVersion'],
}
else:
for item in train.get('items') or ():
yield {
'app_id': app['id'],
'platform': platform,
'version': train['versionString'],
'build_id': item['buildVersion'],
}

def _fetch_build_history_with_train(self, app, version, platform):
rv = self._session.get(urljoin(
API_BASE, 'ra/apps/%s/trains/%s/buildHistory?platform=%s' % (
app['id'], version, platform)))
rv.raise_for_status()
return rv.json().get('data', {}).get('items', [])

def get_dsym_url(self, app_id, platform, version, build_id):
"""Looks up the dsym URL for a given build"""
rv = self._session.get(urljoin(
API_BASE, 'ra/apps/%s/platforms/%s/trains/%s/builds/%s/details' % (
app_id, platform, version, build_id)))
rv.raise_for_status()
return rv.json()['data']['dsymurl']

def _list_apps(self):
rv = self._session.get(urljoin(
API_BASE, 'ra/apps/manageyourapps/summary/v2'))
rv.raise_for_status()
apps = rv.json()['data']['summaries']

rv = []
for app in apps:
platforms = set()
for x in app['versionSets']:
if x['type'] == 'APP':
platforms.add(x['platformString'])
rv.append({
'id': app['adamId'],
'icon_url': app['iconUrl'],
'bundle_id': app['bundleId'],
'name': app['name'],
'platforms': sorted(platforms),
})

return rv

def close(self):
self._session.close()

def __enter__(self):
return self

def __exit__(self, exc_type, exc_value, tb):
self.close()
1 change: 1 addition & 0 deletions src/sentry_plugins/itunesconnect/endpoints/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from __future__ import absolute_import
58 changes: 58 additions & 0 deletions src/sentry_plugins/itunesconnect/endpoints/itunesconnect.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from __future__ import absolute_import

from sentry.utils import json
from sentry.plugins.endpoints import PluginProjectEndpoint


class ItunesConnectAppSyncEndpoint(PluginProjectEndpoint):

def post(self, request, project, *args, **kwargs):
client = self.plugin.get_client(project=project)
app_id = json.loads(request.body.decode('utf-8')).get('app_id')
apps = set(client.apps_to_sync)
if app_id in apps:
apps.remove(app_id)
else:
apps.add(app_id)
client.apps_to_sync = list(apps)
client.save()
return self.respond({
'result': {'apps': client.get_apps()}
})


class ItunesConnectTestConfigEndpoint(PluginProjectEndpoint):

def get(self, request, project, *args, **kwargs):
client = self.plugin.get_client(project=project)
return self.respond({
'result': {'apps': client.get_apps()}
})

def post(self, request, project, *args, **kwargs):
client = self.plugin.get_client(project=project)
result = None
try:
api_client = self.plugin.get_api_client(project=project)
api_client = self.plugin.login(project=project, api_client=api_client)
result = api_client.get_user_details()
except Exception as exc:
error = True
message = 'There was an error connecting to iTunes Connect.'
exception = repr(exc)
self.plugin.reset_client(project=project)
else:
error = False
message = 'No errors returned'
exception = None
client.apps = result.get('apps', {})
client.save()
result['apps'] = client.get_apps()
self.plugin.store_client(project=project, api_client=api_client)

return self.respond({
'message': message,
'result': result,
'error': error,
'exception': exception,
})
Loading