Skip to content

Commit c43cc32

Browse files
committed
🚧 Automatic creation of private keys using webscraping
fixes #14
1 parent 228ea5b commit c43cc32

File tree

4 files changed

+116
-13
lines changed

4 files changed

+116
-13
lines changed

git_repo/services/ext/bitbucket.py

Lines changed: 101 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818

1919
from requests import Request, Session
2020
from requests.exceptions import HTTPError
21-
import os, json
21+
from lxml import html
22+
import os, json, platform
2223

2324

2425
@register_target('bb', 'bitbucket')
@@ -30,11 +31,16 @@ def __init__(self, *args, **kwarg):
3031
super(BitbucketService, self).__init__(*args, **kwarg)
3132

3233
def connect(self):
33-
if not self._privatekey:
34+
if self._privatekey and ':' in self._privatekey:
35+
login, password = self._privatekey.split(':')
36+
else:
37+
login = self._username
38+
password = self._privatekey
39+
40+
if not login or not password:
3441
raise ConnectionError('Could not connect to BitBucket. Please configure .gitconfig with your bitbucket credentials.')
35-
if not ':' in self._privatekey:
36-
raise ConnectionError('Could not connect to BitBucket. Please setup your private key with login:password')
37-
auth = BasicAuthenticator(*self._privatekey.split(':')+['[email protected]'])
42+
43+
auth = BasicAuthenticator(login, password, '[email protected]')
3844
self.bb.client.config = auth
3945
self.bb.client.session = self.bb.client.config.session = auth.start_http_session(self.bb.client.session)
4046
try:
@@ -353,8 +359,96 @@ def request_fetch(self, user, repo, request, pull=False):
353359

354360
@classmethod
355361
def get_auth_token(cls, login, password, prompt=None):
356-
log.warn("/!\\ Due to API limitations, the bitbucket login/password is stored as plaintext in configuration.")
357-
return "{}:{}".format(login, password)
362+
session = Session()
363+
364+
key_name = 'git-repo@{}'.format(platform.node())
365+
366+
# get login page
367+
log.info('» Login to bitbucket…')
368+
369+
login_url = "https://bitbucket.org/account/signin/?next=/".format(login)
370+
371+
session.headers.update({
372+
"User-Agent":"Mozilla/5.0 (X11; Linux x86_64) " \
373+
"AppleWebKit/537.36 (KHTML, like Gecko) " \
374+
"Chrome/52.0.2743.82 Safari/537.36"
375+
})
376+
377+
result = session.get(login_url)
378+
tree = html.fromstring(result.text)
379+
380+
# extract CSRF token
381+
382+
authenticity_token = list(set(tree.xpath("//input[@name='csrfmiddlewaretoken']/@value")))[0]
383+
384+
# do login
385+
386+
payload = {
387+
'username': login,
388+
'password': password,
389+
'csrfmiddlewaretoken': authenticity_token
390+
}
391+
392+
result = session.post(
393+
login_url,
394+
data = payload,
395+
headers = dict(referer=login_url)
396+
)
397+
tree = html.fromstring(result.text)
398+
399+
# extract username
400+
401+
try:
402+
username = json.loads(tree.xpath('//meta/@data-current-user')[0])['username']
403+
except KeyError:
404+
raise ResourceNotFoundError('Invalid login. Please make sure you\'re using your bitbucket email address as username!')
405+
406+
app_password_url ='https://bitbucket.org/account/user/{}/app-passwords/new'.format(username)
407+
408+
# load app password page
409+
log.info('» Generating app password…')
410+
411+
result = session.get(
412+
app_password_url,
413+
headers=dict(referer=app_password_url)
414+
)
415+
tree = html.fromstring(result.content)
416+
417+
if 'git-repo@{}'.format(platform.node()) in result:
418+
log.warn("A duplicate key is being created!")
419+
420+
# generate app password
421+
log.info('» App password is setup with following scopes:')
422+
log.info('» account, team, project:write, repository:admin, repository:delete')
423+
log.info('» pullrequest:write, snippet, snippet:write')
424+
425+
authenticity_token = list(set(tree.xpath("//input[@name='csrfmiddlewaretoken']/@value")))[0]
426+
427+
payload = dict(
428+
name=key_name,
429+
scope=['account',
430+
'team',
431+
'project',
432+
'project:write',
433+
'repository',
434+
'pullrequest:write',
435+
'repository:admin',
436+
'repository:delete',
437+
'snippet',
438+
'snippet:write'
439+
],
440+
csrfmiddlewaretoken=authenticity_token
441+
)
442+
result = session.post(
443+
app_password_url,
444+
data=payload,
445+
headers=dict(referer=app_password_url)
446+
)
447+
tree = html.fromstring(result.content)
448+
449+
password = json.loads(tree.xpath('//section/@data-app-password')[0])['password']
450+
451+
return password, username
358452

359453
@property
360454
def user(self):
@@ -364,4 +458,3 @@ def user(self):
364458
except (HTTPError, AttributeError) as err:
365459
raise ResourceError("Couldn't find the current user: {}".format(err)) from err
366460

367-

git_repo/services/service.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ def load_configuration(self, c, hc=[]):
167167
c.get('token',
168168
c.get('private_token',
169169
c.get('privatekey', None))))
170+
self._username = os.environ.get('USERNAME_{}'.format(self.name.upper()),
171+
c.get('username', None))
170172
self._alias = c.get('alias', self.name)
171173

172174
self.fqdn = c.get('fqdn', self.fqdn)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
docopt
22
progress
33
python-dateutil
4+
lxml
45
GitPython>=2.1.0
56
uritemplate.py==2.0.0
67
github3.py==0.9.5

tests/conftest.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,13 @@
1616
# also if an environment variable is not set, then we don't want to record cassettes
1717
record_mode = 'never'
1818
for service in services:
19+
user_name = 'USERNAME_{}'.format(service.upper())
1920
token_name = 'PRIVATE_KEY_{}'.format(service.upper())
2021
namespace_name = '{}_NAMESPACE'.format(service.upper())
22+
if user_name not in os.environ:
23+
os.environ[user_name] = '_username_'.format(service)
2124
if token_name not in os.environ:
22-
os.environ[token_name] = '_namespace_{}_:_private_'.format(service) # using a : for bitbucket's case
25+
os.environ[token_name] = '_token_{}_'.format(service)
2326
if namespace_name not in os.environ:
2427
os.environ[namespace_name] = '_namespace_{}_'.format(service)
2528
else:
@@ -31,21 +34,25 @@
3134

3235
# handle the different forms of token configuration item (yup, technical debt bites here)
3336
get_section = lambda s: 'gitrepo "{}"'.format(s)
37+
get_username = lambda s: config.get_value(get_section(s), 'username',
38+
'_username_{}'.format(s)
39+
)
3440
get_token = lambda s: config.get_value(get_section(s), 'token',
3541
config.get_value(get_section(s), 'private_token',
3642
config.get_value(get_section(s), 'privatekey',
37-
'_namespace_{}_:_private_'.format(s) # using a : for bitbucket's case
43+
'_namespace_{}_'.format(s)
3844
)))
39-
# XXX temporary fix that should not be necessary when refactoring with pybitbucket
40-
get_default_namespace = lambda s: os.environ[token_name].split(':')[0] if s == 'bitbucket' else '_namespace_{}_'.format(s)
4145

4246
for service in services:
47+
user_name = 'USERNAME_{}'.format(service.upper())
4348
token_name = 'PRIVATE_KEY_{}'.format(service.upper())
4449
namespace_name = '{}_NAMESPACE'.format(service.upper())
50+
if user_name not in os.environ:
51+
os.environ[user_name] = get_username(service)
4552
if token_name not in os.environ:
4653
os.environ[token_name] = get_token(service)
4754
if namespace_name not in os.environ:
48-
os.environ[namespace_name] = os.environ.get('GITREPO_NAMESPACE', get_default_namespace(service))
55+
os.environ[namespace_name] = os.environ.get('GITREPO_NAMESPACE', '_namespace_{}_'.format(service))
4956

5057
betamax.Betamax.register_serializer(pretty_json.PrettyJSONSerializer)
5158

0 commit comments

Comments
 (0)