sapling/phabricator/conduit.py
Mark Edson 36d60f5259 phabricator: make conduit auth more flexible
Summary:
We've modified arcanist a while ago so that if it doesn't find user/cert information for the specific URL that it's using, it just picks the user/cert information from any other URL in ~/.arcrc.  This is because it's essentially always the same user, and always the same cert, so there's really no point in being too picky.
This updates hg extension to be almost as careless.  It will attempt to find the matching user/cert, but if it doesn't work, it'll just pick any cert if available.

Test Plan:
Without this change, "hg ssl" in a recent version of www reports an warning because www/.arcconfig has phabricator.intern.facebook.com, where default ~/.arcrc's have phabricator.fb.com.
With this change, "hg ssl" succeeds to display revision information because it is once again able to authenticate with conduit

Reviewers: #phabricator, rmcelroy

Reviewed By: rmcelroy

Subscribers: mjpieters

Differential Revision: https://phabricator.intern.facebook.com/D4964111

Tasks: 17683952

Signature: t1:4964111:1493323636:668b50ce2d20d720ba3de573de05be5251ce3310
2017-04-27 13:45:50 -07:00

132 lines
4.6 KiB
Python

# conduit.py
#
# A library function to call a phabricator conduit RPC.
# It's different from fbconduit in that this is an authenticated
# conduit client.
import hashlib
from mercurial.util import httplib
import json
import os
import time
from mercurial import util
import arcconfig
urlreq = util.urlreq
DEFAULT_HOST = 'https://phabricator.intern.facebook.com/api/'
DEFAULT_TIMEOUT = 60
mocked_responses = None
class ClientError(Exception):
def __init__(self, code, msg):
Exception.__init__(self, msg)
self.code = code
class Client(object):
def __init__(self, host=None, user=None, cert=None, act_as=None):
self._host = host or DEFAULT_HOST
self._user = user
self._cert = cert
self._actas = act_as or self._user
self._connection = None
def apply_arcconfig(self, config):
self._host = config.get('conduit_uri', DEFAULT_HOST)
try:
hostconfig = config['hosts'][self._host]
self._user = hostconfig['user']
self._cert = hostconfig['cert']
except KeyError:
try:
hostconfig = config['hosts'][config['hosts'].keys()[0]]
self._user = hostconfig['user']
self._cert = hostconfig['cert']
except KeyError:
raise arcconfig.ArcConfigError(
'arcrc is missing user '
'credentials for host %s. use '
'"arc install-certificate" to fix.' % self._host)
self._actas = self._user
self._connection = None
def call(self, method, args, timeout=DEFAULT_TIMEOUT):
token = '%d' % time.time()
sig = token + self._cert
args['__conduit__'] = {
'authUser': self._user,
'actAsUser': self._actas,
'authToken': token,
'authSignature': hashlib.sha1(sig.encode('utf-8')).hexdigest(),
}
req_data = util.urlreq.urlencode(
{
'params': json.dumps(args),
'output': 'json',
}
)
urlparts = urlreq.urlparse(self._host)
if self._connection is None:
if urlparts.scheme == 'http':
self._connection = httplib.HTTPConnection(
urlparts.netloc, timeout=timeout)
elif urlparts.scheme == 'https':
self._connection = httplib.HTTPSConnection(
urlparts.netloc, timeout=timeout)
else:
raise ClientError(
None, 'Unknown host scheme: %s', urlparts.scheme)
# self._connection.set_debuglevel(1)
self._connection.request('POST', (urlparts.path + method), req_data,
{'Connection': 'Keep-Alive'})
response = json.load(self._connection.getresponse())
if response['error_code'] is not None:
raise ClientError(response['error_code'], response['error_info'])
return response['result']
class MockClient(object):
def __init__(self, **kwargs):
pass
def apply_arcconfig(self, config):
pass
def call(self, method, args, timeout=DEFAULT_TIMEOUT):
global mocked_responses
cmd = json.dumps([method, args], sort_keys=True)
try:
response = mocked_responses.pop(0)
# Check expectations via a deep compare of the json representation.
# We need this because child objects and values are compared by
# address rather than value.
expect = json.dumps(response.get('cmd', None), sort_keys=True)
if cmd != expect:
raise ClientError(None,
'mock mismatch got %s expected %s' % (
cmd, expect))
if 'error_info' in response:
raise ClientError(response.get('error_code', None),
response['error_info'])
return response['result']
except IndexError:
raise ClientError(None,
'No more mocked responses available for call to %s' % cmd)
if 'HG_ARC_CONDUIT_MOCK' in os.environ:
# To facilitate testing, we replace the client object with this
# fake implementation that returns responses from a file that
# contains a series of json serialized object values.
with open(os.environ['HG_ARC_CONDUIT_MOCK'], 'r') as f:
mocked_responses = json.load(f)
Client = MockClient
def call_conduit(method, args, timeout=DEFAULT_TIMEOUT):
client = Client()
client.apply_arcconfig(arcconfig.load_for_path(os.getcwd()))
return client.call(method, args, timeout=timeout)