Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
58 changes: 26 additions & 32 deletions social/backends/bitbucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,51 +9,45 @@
class BitbucketOAuth(BaseOAuth1):
"""Bitbucket OAuth authentication backend"""
name = 'bitbucket'
ID_KEY = 'username'
AUTHORIZATION_URL = 'https://bitbucket.org/api/1.0/oauth/authenticate'
REQUEST_TOKEN_URL = 'https://bitbucket.org/api/1.0/oauth/request_token'
ACCESS_TOKEN_URL = 'https://bitbucket.org/api/1.0/oauth/access_token'
EXTRA_DATA = [
('username', 'username'),
('expires', 'expires'),
('email', 'email'),
('first_name', 'first_name'),
('last_name', 'last_name')
]

# Bitbucket usernames can change. The account ID should always be the UUID
# See: https://confluence.atlassian.com/display/BITBUCKET/Use+the+Bitbucket+REST+APIs
ID_KEY = 'uuid'

def get_user_details(self, response):
"""Return user details from Bitbucket account"""
fullname, first_name, last_name = self.get_user_names(
first_name=response.get('first_name', ''),
last_name=response.get('last_name', '')
)
return {'username': response.get('username') or '',
'email': response.get('email') or '',
fullname, first_name, last_name = self.get_user_names(response['display_name'])

return {'username': response.get('username', ''),
'email': response.get('email', ''),
'fullname': fullname,
'first_name': first_name,
'last_name': last_name}

def user_data(self, access_token):
"""Return user data provided"""
# Bitbucket has a bit of an indirect route to obtain user data from an
# authenticated query: First obtain the user's email via an
# authenticated GET, then retrieve the user's primary email address or
# the top email
emails = self.get_json('https://bitbucket.org/api/1.0/emails/',
emails = self.get_json('https://api.bitbucket.org/2.0/user/emails',
auth=self.oauth_auth(access_token))

email = None
for address in reversed(emails):
if address['active']:
email = address['email']
if address['primary']:
break

for address in reversed(emails['values']):
email = address['email']
if address['is_primary']:
break

if self.setting('VERIFIED_EMAILS_ONLY', False) and not address['is_confirmed']:
raise AuthForbidden(
self, 'Bitbucket account has no verified email'
)

user = self.get_json('https://api.bitbucket.org/2.0/user',
auth=self.oauth_auth(access_token))

if email:
return dict(self.get_json('https://bitbucket.org/api/1.0/users/' +
email)['user'],
email=email)
elif self.setting('VERIFIED_EMAILS_ONLY', False):
raise AuthForbidden(self,
'Bitbucket account has any verified email')
else:
return {}
user['email'] = email

return user
79 changes: 53 additions & 26 deletions social/tests/backends/test_bitbucket.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

class BitbucketOAuth1Test(OAuth1Test):
backend_path = 'social.backends.bitbucket.BitbucketOAuth'
user_data_url = 'https://bitbucket.org/api/1.0/users/foo@bar.com'
user_data_url = 'https://api.bitbucket.org/2.0/user'
expected_username = 'foobar'
access_token_body = json.dumps({
'access_token': 'foobar',
Expand All @@ -20,46 +20,73 @@ class BitbucketOAuth1Test(OAuth1Test):
'oauth_token': 'foobar',
'oauth_callback_confirmed': 'true'
})
emails_body = json.dumps([{
'active': True,
'email': 'foo@bar.com',
'primary': True
}])
emails_body = json.dumps({
u'page': 1,
u'pagelen': 10,
u'size': 2,
u'values': [
{
u'email': u'foo@bar.com',
u'is_confirmed': True,
u'is_primary': True,
u'links': { u'self': {u'href': u'https://api.bitbucket.org/2.0/user/emails/foo@bar.com'}},
u'type': u'email'
},
{
u'email': u'not@confirme.com',
u'is_confirmed': False,
u'is_primary': False,
u'links': {u'self': {u'href': u'https://api.bitbucket.org/2.0/user/emails/not@confirmed.com'}},
u'type': u'email'
}
]
})
user_data_body = json.dumps({
'user': {
'username': 'foobar',
'first_name': 'Foo',
'last_name': 'Bar',
'display_name': 'Foo Bar',
'is_team': False,
'avatar': 'https://secure.gravatar.com/avatar/'
'5280f15cedf540b544eecc30fcf3027c?'
'd=https%3A%2F%2Fd3oaxc4q5k2d6q.cloudfront.net%2Fm%2F'
'9e262ba34f96%2Fimg%2Fdefault_avatar%2F32%2F'
'user_blue.png&s=32',
'resource_uri': '/1.0/users/foobar'
}
u'created_on': u'2012-03-29T18:07:38+00:00',
u'display_name': u'Foo Bar',
u'links': {
u'avatar': {u'href': u'https://bitbucket.org/account/foobar/avatar/32/'},
u'followers': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/followers'},
u'following': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/following'},
u'hooks': {u'href': u'https://api.bitbucket.org/2.0/users/foobar/hooks'},
u'html': {u'href': u'https://bitbucket.org/foobar'},
u'repositories': {u'href': u'https://api.bitbucket.org/2.0/repositories/foobar'},
u'self': {u'href': u'https://api.bitbucket.org/2.0/users/foobar'}},
u'location': u'Fooville, Bar',
u'type': u'user',
u'username': u'foobar',
u'uuid': u'{397621dc-0f78-329f-8d6d-727396248e3f}',
u'website': u'http://foobar.com'
})

def test_login(self):
HTTPretty.register_uri(HTTPretty.GET,
'https://bitbucket.org/api/1.0/emails/',
'https://api.bitbucket.org/2.0/user/emails',
status=200, body=self.emails_body)
self.do_login()

def test_partial_pipeline(self):
HTTPretty.register_uri(HTTPretty.GET,
'https://bitbucket.org/api/1.0/emails/',
'https://api.bitbucket.org/2.0/user/emails',
status=200, body=self.emails_body)
self.do_partial_pipeline()


class BitbucketOAuth1FailTest(BitbucketOAuth1Test):
emails_body = json.dumps([{
'active': False,
'email': 'foo@bar.com',
'primary': True
}])
emails_body = json.dumps({
u'page': 1,
u'pagelen': 10,
u'size': 1,
u'values': [
{
u'email': u'foo@bar.com',
u'is_confirmed': False,
u'is_primary': True,
u'links': { u'self': {u'href': u'https://api.bitbucket.org/2.0/user/emails/foo@bar.com'}},
u'type': u'email'
}
]
})

def test_login(self):
self.strategy.set_settings({
Expand Down