# HG changeset patch # User Marcin Kuzminski # Date 1313853234 -10800 # Node ID c78f6bf52e9c26cf154cc0c1f6e2d15e4a230be4 # Parent d17aa79768f09c0078770e100d4dccca6892a63c Beginning of API implementation for rhodecode diff -r d17aa79768f0 -r c78f6bf52e9c rhodecode/config/routing.py --- a/rhodecode/config/routing.py Mon Aug 15 19:53:43 2011 +0300 +++ b/rhodecode/config/routing.py Sat Aug 20 18:13:54 2011 +0300 @@ -254,6 +254,7 @@ m.connect("admin_settings_create_repository", "/create_repository", action="create_repository", conditions=dict(method=["GET"])) + #ADMIN MAIN PAGES with rmap.submapper(path_prefix=ADMIN_PREFIX, controller='admin/admin') as m: @@ -261,6 +262,14 @@ m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}', action='add_repo') + #========================================================================== + # API V1 + #========================================================================== + with rmap.submapper(path_prefix=ADMIN_PREFIX, + controller='api/api') as m: + m.connect('api', '/api') + + #USER JOURNAL rmap.connect('journal', '%s/journal' % ADMIN_PREFIX, controller='journal') @@ -400,4 +409,5 @@ rmap.connect('repo_forks_home', '/{repo_name:.*}/forks', controller='forks', action='forks', conditions=dict(function=check_repo)) + return rmap diff -r d17aa79768f0 -r c78f6bf52e9c rhodecode/controllers/api/__init__.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/controllers/api/__init__.py Sat Aug 20 18:13:54 2011 +0300 @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.controllers.api + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + JSON RPC controller + + :created_on: Aug 20, 2011 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: GPLv3, see COPYING for more details. +""" +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; version 2 +# of the License or (at your opinion) any later version of the license. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. + +import inspect +import json +import logging +import types +import urllib + +from paste.response import replace_header + +from pylons.controllers import WSGIController +from pylons.controllers.util import Response + +from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \ +HTTPBadRequest, HTTPError + +from rhodecode.model.user import User +from rhodecode.lib.auth import AuthUser + +log = logging.getLogger('JSONRPC') + +class JSONRPCError(BaseException): + + def __init__(self, message): + self.message = message + + def __str__(self): + return str(self.message) + + +def jsonrpc_error(message, code=None): + """Generate a Response object with a JSON-RPC error body""" + return Response(body=json.dumps(dict(result=None, + error=message))) + + +class JSONRPCController(WSGIController): + """ + A WSGI-speaking JSON-RPC controller class + + See the specification: + `. + + Valid controller return values should be json-serializable objects. + + Sub-classes should catch their exceptions and raise JSONRPCError + if they want to pass meaningful errors to the client. + + """ + + def _get_method_args(self): + """ + Return `self._rpc_args` to dispatched controller method + chosen by __call__ + """ + return self._rpc_args + + def __call__(self, environ, start_response): + """ + Parse the request body as JSON, look up the method on the + controller and if it exists, dispatch to it. + """ + + if 'CONTENT_LENGTH' not in environ: + log.debug("No Content-Length") + return jsonrpc_error(0, "No Content-Length") + else: + length = environ['CONTENT_LENGTH'] or 0 + length = int(environ['CONTENT_LENGTH']) + log.debug('Content-Length: %s', length) + + if length == 0: + log.debug("Content-Length is 0") + return jsonrpc_error(0, "Content-Length is 0") + + raw_body = environ['wsgi.input'].read(length) + + try: + json_body = json.loads(urllib.unquote_plus(raw_body)) + except ValueError as e: + #catch JSON errors Here + return jsonrpc_error("JSON parse error ERR:%s RAW:%r" \ + % (e, urllib.unquote_plus(raw_body))) + + + #check AUTH based on API KEY + + try: + self._req_api_key = json_body['api_key'] + self._req_method = json_body['method'] + self._req_params = json_body['args'] + log.debug('method: %s, params: %s', + self._req_method, + self._req_params) + except KeyError as e: + return jsonrpc_error(message='Incorrect JSON query missing %s' % e) + + #check if we can find this session using api_key + try: + u = User.get_by_api_key(self._req_api_key) + auth_u = AuthUser(u.user_id, self._req_api_key) + except Exception as e: + return jsonrpc_error(message='Invalid API KEY') + + self._error = None + try: + self._func = self._find_method() + except AttributeError, e: + return jsonrpc_error(str(e)) + + # now that we have a method, add self._req_params to + # self.kargs and dispatch control to WGIController + arglist = inspect.getargspec(self._func)[0][1:] + + # this is little trick to inject logged in user for + # perms decorators to work they expect the controller class to have + # rhodecode_user set + self.rhodecode_user = auth_u + + if 'user' not in arglist: + return jsonrpc_error('This method [%s] does not support ' + 'authentication (missing user param)' % + self._func.__name__) + + # get our arglist and check if we provided them as args + for arg in arglist: + if arg == 'user': + # user is something translated from api key and this is + # checked before + continue + + if not self._req_params or arg not in self._req_params: + return jsonrpc_error('Missing %s arg in JSON DATA' % arg) + + self._rpc_args = dict(user=u) + self._rpc_args.update(self._req_params) + + self._rpc_args['action'] = self._req_method + self._rpc_args['environ'] = environ + self._rpc_args['start_response'] = start_response + + status = [] + headers = [] + exc_info = [] + def change_content(new_status, new_headers, new_exc_info=None): + status.append(new_status) + headers.extend(new_headers) + exc_info.append(new_exc_info) + + output = WSGIController.__call__(self, environ, change_content) + output = list(output) + headers.append(('Content-Length', str(len(output[0])))) + replace_header(headers, 'Content-Type', 'application/json') + start_response(status[0], headers, exc_info[0]) + + return output + + def _dispatch_call(self): + """ + Implement dispatch interface specified by WSGIController + """ + try: + raw_response = self._inspect_call(self._func) + print raw_response + if isinstance(raw_response, HTTPError): + self._error = str(raw_response) + except JSONRPCError as e: + self._error = str(e) + except Exception as e: + log.debug('Encountered unhandled exception: %s', repr(e)) + json_exc = JSONRPCError('Internal server error') + self._error = str(json_exc) + + if self._error is not None: + raw_response = None + + response = dict(result=raw_response, error=self._error) + + try: + return json.dumps(response) + except TypeError, e: + log.debug('Error encoding response: %s', e) + return json.dumps(dict(result=None, + error="Error encoding response")) + + def _find_method(self): + """ + Return method named by `self._req_method` in controller if able + """ + log.debug('Trying to find JSON-RPC method: %s', self._req_method) + if self._req_method.startswith('_'): + raise AttributeError("Method not allowed") + + try: + func = getattr(self, self._req_method, None) + except UnicodeEncodeError: + raise AttributeError("Problem decoding unicode in requested " + "method name.") + + if isinstance(func, types.MethodType): + return func + else: + raise AttributeError("No such method: %s" % self._req_method) diff -r d17aa79768f0 -r c78f6bf52e9c rhodecode/controllers/api/api.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/controllers/api/api.py Sat Aug 20 18:13:54 2011 +0300 @@ -0,0 +1,40 @@ +from rhodecode.controllers.api import JSONRPCController, JSONRPCError +from rhodecode.lib.auth import HasPermissionAllDecorator +from rhodecode.model.scm import ScmModel + + +class ApiController(JSONRPCController): + """ + API Controller + + + Each method needs to have USER as argument this is then based on given + API_KEY propagated as instance of user object + + Preferably this should be first argument also + + + Each function should also **raise** JSONRPCError for any + errors that happens + + """ + + @HasPermissionAllDecorator('hg.admin') + def pull(self, user, repo): + """ + Dispatch pull action on given repo + + + param user: + param repo: + """ + + try: + ScmModel().pull_changes(repo, self.rhodecode_user.username) + return 'Pulled from %s' % repo + except Exception: + raise JSONRPCError('Unable to pull changes from "%s"' % repo) + + + +