diff --git a/bugwarrior/data.py b/bugwarrior/data.py new file mode 100644 index 000000000..1f66a246d --- /dev/null +++ b/bugwarrior/data.py @@ -0,0 +1,26 @@ +import os +import json + + +DATAFILE = os.path.expanduser( + os.path.join(os.getenv('TASKDATA', '~/.task'), 'bugwarrior.data')) + + +def get(key): + try: + with open(DATAFILE, 'r') as jsondata: + data = json.load(jsondata) + return data[key] + except IOError: # File does not exist. + return None + + +def set(key, value): + try: + with open(DATAFILE, 'rw') as jsondata: + data = json.load(jsondata) + data[key] = value + json.dump(data, jsondata) + except IOError: # File does not exist. + with open(DATAFILE, 'w+') as jsondata: + json.dump({key: value}, jsondata) diff --git a/bugwarrior/docs/services/bitbucket.rst b/bugwarrior/docs/services/bitbucket.rst index 66b60cc57..7705b6dc8 100644 --- a/bugwarrior/docs/services/bitbucket.rst +++ b/bugwarrior/docs/services/bitbucket.rst @@ -27,6 +27,14 @@ set to ralphbean (my account). But I have some targets with ``bitbucket.username`` pointed at organizations or other users to watch issues there. +As an alternative to password authentication, there is OAuth. To get a key and secret, +go to the "OAuth" section of your profile settings and click "Add consumer". Set the +"Callback URL" to ``https://localhost/`` and set the appropriate permissions. Then +assign your consumer's credentials to ``bitbucket.key`` and ``bitbucket.secret``. Note +that you will have to provide a password (only) the first time you pull, so you may +want to set ``bitbucket.password = @oracle:ask_password`` and run +``bugwarrior-pull --interactive`` on your next pull. + Service Features ---------------- diff --git a/bugwarrior/services/bitbucket.py b/bugwarrior/services/bitbucket.py index cd1d35d34..78cbffb87 100644 --- a/bugwarrior/services/bitbucket.py +++ b/bugwarrior/services/bitbucket.py @@ -1,6 +1,7 @@ import requests from twiggy import log +from bugwarrior import data from bugwarrior.services import IssueService, Issue from bugwarrior.config import asbool, die @@ -65,11 +66,36 @@ class BitbucketService(IssueService): def __init__(self, *args, **kw): super(BitbucketService, self).__init__(*args, **kw) - self.auth = None - login = self.config_get('login') - password = self.config_get_password('password', login) - self.auth = (login, password) + key = self.config_get_default('key') + secret = self.config_get_default('secret') + self.auth = {'oauth': (key, secret)} + + refresh_token = data.get('bitbucket_refresh_token') + + if not refresh_token: + login = self.config_get('login') + password = self.config_get_password('password', login) + self.auth['basic'] = (login, password) + + if key and secret: + if refresh_token: + response = requests.post( + self.BASE_URL + 'site/oauth2/access_token', + data={'grant_type': 'refresh_token', + 'refresh_token': refresh_token}, + auth=self.auth['oauth']).json() + else: + response = requests.post( + self.BASE_URL + 'site/oauth2/access_token', + data={'grant_type': 'password', + 'username': login, + 'password': password}, + auth=self.auth['oauth']).json() + + data.set('bitbucket_refresh_token', response['refresh_token']) + + self.auth['token'] = response['access_token'] self.exclude_repos = [] if self.config_get_default('exclude_repos', None): @@ -112,7 +138,15 @@ def filter_repos(self, repo_tag): def get_data(self, url, **kwargs): api = kwargs.get('api', self.BASE_API2) - response = requests.get(api + url, auth=self.auth) + + kwargs = {} + if 'token' in self.auth: + kwargs['headers'] = { + 'Authorization': 'Bearer ' + self.auth['token']} + elif 'basic' in self.auth: + kwargs['auth'] = self.auth['basic'] + + response = requests.get(api + url, **kwargs) # And.. if we didn't get good results, just bail. if response.status_code != 200: