##// END OF EJS Templates
changed API to match fully JSON-RPC specs
marcink -
r1708:fee9895f beta
parent child Browse files
Show More
@@ -1,362 +1,365 b''
1 1 .. _api:
2 2
3 3
4 4 API
5 5 ===
6 6
7 7
8 8 Starting from RhodeCode version 1.2 a simple API was implemented.
9 9 There's a single schema for calling all api methods. API is implemented
10 10 with JSON protocol both ways. An url to send API request in RhodeCode is
11 11 <your_server>/_admin/api
12 12
13 13
14 All clients need to send JSON data in such format::
14 All clients are required to send JSON-RPC spec JSON data::
15 15
16 {
16 {
17 "id:<id>,
17 18 "api_key":"<api_key>",
18 19 "method":"<method_name>",
19 20 "args":{"<arg_key>":"<arg_val>"}
20 21 }
21 22
22 23 Example call for autopulling remotes repos using curl::
23 curl https://server.com/_admin/api -X POST -H 'content-type:text/plain' --data-binary '{"api_key":"xe7cdb2v278e4evbdf5vs04v832v0efvcbcve4a3","method":"pull","args":{"repo":"CPython"}}'
24 curl https://server.com/_admin/api -X POST -H 'content-type:text/plain' --data-binary '{"id":1,"api_key":"xe7cdb2v278e4evbdf5vs04v832v0efvcbcve4a3","method":"pull","args":{"repo":"CPython"}}'
24 25
25 26 Simply provide
27 - *id* A value of any type, which is used to match the response with the request that it is replying to.
26 28 - *api_key* for access and permission validation.
27 29 - *method* is name of method to call
28 30 - *args* is an key:value list of arguments to pass to method
29 31
30 32 .. note::
31 33
32 34 api_key can be found in your user account page
33 35
34 36
35 RhodeCode API will return always a JSON formatted answer::
37 RhodeCode API will return always a JSON-RPC response::
36 38
37 {
39 {
40 "id":<id>,
38 41 "result": "<result>",
39 42 "error": null
40 43 }
41 44
42 45 All responses from API will be `HTTP/1.0 200 OK`, if there's an error while
43 46 calling api *error* key from response will contain failure description
44 47 and result will be null.
45 48
46 49 API METHODS
47 50 +++++++++++
48 51
49 52
50 53 pull
51 54 ----
52 55
53 56 Pulls given repo from remote location. Can be used to automatically keep
54 57 remote repos up to date. This command can be executed only using api_key
55 58 belonging to user with admin rights
56 59
57 60 INPUT::
58 61
59 62 api_key : "<api_key>"
60 63 method : "pull"
61 64 args : {
62 65 "repo" : "<repo_name>"
63 66 }
64 67
65 68 OUTPUT::
66 69
67 70 result : "Pulled from <repo_name>"
68 71 error : null
69 72
70 73
71 74 get_users
72 75 ---------
73 76
74 77 Lists all existing users. This command can be executed only using api_key
75 78 belonging to user with admin rights.
76 79
77 80 INPUT::
78 81
79 82 api_key : "<api_key>"
80 83 method : "get_users"
81 84 args : { }
82 85
83 86 OUTPUT::
84 87
85 88 result: [
86 89 {
87 90 "id" : "<id>",
88 91 "username" : "<username>",
89 92 "firstname": "<firstname>",
90 93 "lastname" : "<lastname>",
91 94 "email" : "<email>",
92 95 "active" : "<bool>",
93 96 "admin" :Β  "<bool>",
94 97 "ldap" : "<ldap_dn>"
95 98 },
96 99 …
97 100 ]
98 101 error: null
99 102
100 103 create_user
101 104 -----------
102 105
103 106 Creates new user in RhodeCode. This command can be executed only using api_key
104 107 belonging to user with admin rights.
105 108
106 109 INPUT::
107 110
108 111 api_key : "<api_key>"
109 112 method : "create_user"
110 113 args : {
111 114 "username" : "<username>",
112 115 "password" : "<password>",
113 116 "firstname" : "<firstname>",
114 117 "lastname" : "<lastname>",
115 118 "email" : "<useremail>"
116 119 "active" : "<bool> = True",
117 120 "admin" : "<bool> = False",
118 121 "ldap_dn" : "<ldap_dn> = None"
119 122 }
120 123
121 124 OUTPUT::
122 125
123 126 result: {
124 127 "msg" : "created new user <username>"
125 128 }
126 129 error: null
127 130
128 131 get_users_groups
129 132 ----------------
130 133
131 134 Lists all existing users groups. This command can be executed only using api_key
132 135 belonging to user with admin rights.
133 136
134 137 INPUT::
135 138
136 139 api_key : "<api_key>"
137 140 method : "get_users_groups"
138 141 args : { }
139 142
140 143 OUTPUT::
141 144
142 145 result : [
143 146 {
144 147 "id" : "<id>",
145 148 "name" : "<name>",
146 149 "active": "<bool>",
147 150 "members" : [
148 151 {
149 152 "id" : "<userid>",
150 153 "username" : "<username>",
151 154 "firstname": "<firstname>",
152 155 "lastname" : "<lastname>",
153 156 "email" : "<email>",
154 157 "active" : "<bool>",
155 158 "admin" :Β  "<bool>",
156 159 "ldap" : "<ldap_dn>"
157 160 },
158 161 …
159 162 ]
160 163 }
161 164 ]
162 165 error : null
163 166
164 167 get_users_group
165 168 ---------------
166 169
167 170 Gets an existing users group. This command can be executed only using api_key
168 171 belonging to user with admin rights.
169 172
170 173 INPUT::
171 174
172 175 api_key : "<api_key>"
173 176 method : "get_users_group"
174 177 args : {
175 178 "group_name" : "<name>"
176 179 }
177 180
178 181 OUTPUT::
179 182
180 183 result : None if group not exist
181 184 {
182 185 "id" : "<id>",
183 186 "name" : "<name>",
184 187 "active": "<bool>",
185 188 "members" : [
186 189 { "id" : "<userid>",
187 190 "username" : "<username>",
188 191 "firstname": "<firstname>",
189 192 "lastname" : "<lastname>",
190 193 "email" : "<email>",
191 194 "active" : "<bool>",
192 195 "admin" :Β  "<bool>",
193 196 "ldap" : "<ldap_dn>"
194 197 },
195 198 …
196 199 ]
197 200 }
198 201 error : null
199 202
200 203 create_users_group
201 204 ------------------
202 205
203 206 Creates new users group. This command can be executed only using api_key
204 207 belonging to user with admin rights
205 208
206 209 INPUT::
207 210
208 211 api_key : "<api_key>"
209 212 method : "create_users_group"
210 213 args: {
211 214 "name": "<name>",
212 215 "active":"<bool> = True"
213 216 }
214 217
215 218 OUTPUT::
216 219
217 220 result: {
218 221 "id": "<newusersgroupid>",
219 222 "msg": "created new users group <name>"
220 223 }
221 224 error: null
222 225
223 226 add_user_to_users_groups
224 227 ------------------------
225 228
226 229 Adds a user to a users group. This command can be executed only using api_key
227 230 belonging to user with admin rights
228 231
229 232 INPUT::
230 233
231 234 api_key : "<api_key>"
232 235 method : "add_user_users_group"
233 236 args: {
234 237 "group_name" : "<groupname>",
235 238 "user_name" : "<username>"
236 239 }
237 240
238 241 OUTPUT::
239 242
240 243 result: {
241 244 "id": "<newusersgroupmemberid>",
242 245 "msg": "created new users group member"
243 246 }
244 247 error: null
245 248
246 249 get_repos
247 250 ---------
248 251
249 252 Lists all existing repositories. This command can be executed only using api_key
250 253 belonging to user with admin rights
251 254
252 255 INPUT::
253 256
254 257 api_key : "<api_key>"
255 258 method : "get_repos"
256 259 args: { }
257 260
258 261 OUTPUT::
259 262
260 263 result: [
261 264 {
262 265 "id" : "<id>",
263 266 "name" : "<name>"
264 267 "type" : "<type>",
265 268 "description" : "<description>"
266 269 },
267 270 …
268 271 ]
269 272 error: null
270 273
271 274 get_repo
272 275 --------
273 276
274 277 Gets an existing repository. This command can be executed only using api_key
275 278 belonging to user with admin rights
276 279
277 280 INPUT::
278 281
279 282 api_key : "<api_key>"
280 283 method : "get_repo"
281 284 args: {
282 285 "name" : "<name>"
283 286 }
284 287
285 288 OUTPUT::
286 289
287 290 result: None if repository not exist
288 291 {
289 292 "id" : "<id>",
290 293 "name" : "<name>"
291 294 "type" : "<type>",
292 295 "description" : "<description>",
293 296 "members" : [
294 297 { "id" : "<userid>",
295 298 "username" : "<username>",
296 299 "firstname": "<firstname>",
297 300 "lastname" : "<lastname>",
298 301 "email" : "<email>",
299 302 "active" : "<bool>",
300 303 "admin" :Β  "<bool>",
301 304 "ldap" : "<ldap_dn>",
302 305 "permission" : "repository_(read|write|admin)"
303 306 },
304 307 …
305 308 {
306 309 "id" : "<usersgroupid>",
307 310 "name" : "<usersgroupname>",
308 311 "active": "<bool>",
309 312 "permission" : "repository_(read|write|admin)"
310 313 },
311 314 …
312 315 ]
313 316 }
314 317 error: null
315 318
316 319 create_repo
317 320 -----------
318 321
319 322 Creates a repository. This command can be executed only using api_key
320 323 belonging to user with admin rights.
321 324 If repository name contains "/", all needed repository groups will be created.
322 325 For example "foo/bar/baz" will create groups "foo", "bar" (with "foo" as parent),
323 326 and create "baz" repository with "bar" as group.
324 327
325 328 INPUT::
326 329
327 330 api_key : "<api_key>"
328 331 method : "create_repo"
329 332 args: {
330 333 "name" : "<name>",
331 334 "owner_name" : "<ownername>",
332 335 "description" : "<description> = ''",
333 336 "repo_type" : "<type> = 'hg'",
334 337 "private" : "<bool> = False"
335 338 }
336 339
337 340 OUTPUT::
338 341
339 342 result: None
340 343 error: null
341 344
342 345 add_user_to_repo
343 346 ----------------
344 347
345 348 Add a user to a repository. This command can be executed only using api_key
346 349 belonging to user with admin rights.
347 350 If "perm" is None, user will be removed from the repository.
348 351
349 352 INPUT::
350 353
351 354 api_key : "<api_key>"
352 355 method : "add_user_to_repo"
353 356 args: {
354 357 "repo_name" : "<reponame>",
355 358 "user_name" : "<username>",
356 359 "perm" : "(None|repository_(read|write|admin))",
357 360 }
358 361
359 362 OUTPUT::
360 363
361 364 result: None
362 365 error: null
@@ -1,250 +1,253 b''
1 1 # -*- coding: utf-8 -*-
2 2 """
3 3 rhodecode.controllers.api
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 6 JSON RPC controller
7 7
8 8 :created_on: Aug 20, 2011
9 9 :author: marcink
10 10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software; you can redistribute it and/or
14 14 # modify it under the terms of the GNU General Public License
15 15 # as published by the Free Software Foundation; version 2
16 16 # of the License or (at your opinion) any later version of the license.
17 17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program; if not, write to the Free Software
25 25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 26 # MA 02110-1301, USA.
27 27
28 28 import inspect
29 29 import logging
30 30 import types
31 31 import urllib
32 32 import traceback
33 33
34 34 from rhodecode.lib.compat import izip_longest, json
35 35
36 36 from paste.response import replace_header
37 37
38 38 from pylons.controllers import WSGIController
39 39
40 40
41 41 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
42 42 HTTPBadRequest, HTTPError
43 43
44 44 from rhodecode.model.db import User
45 45 from rhodecode.lib.auth import AuthUser
46 46
47 47 log = logging.getLogger('JSONRPC')
48 48
49 49 class JSONRPCError(BaseException):
50 50
51 51 def __init__(self, message):
52 52 self.message = message
53 super(JSONRPCError, self).__init__()
53 54
54 55 def __str__(self):
55 56 return str(self.message)
56 57
57 58
58 59 def jsonrpc_error(message, code=None):
59 60 """
60 61 Generate a Response object with a JSON-RPC error body
61 62 """
62 63 from pylons.controllers.util import Response
63 resp = Response(body=json.dumps(dict(result=None, error=message)),
64 resp = Response(body=json.dumps(dict(id=None, result=None, error=message)),
64 65 status=code,
65 66 content_type='application/json')
66 67 return resp
67 68
68 69
69 70
70 71 class JSONRPCController(WSGIController):
71 72 """
72 73 A WSGI-speaking JSON-RPC controller class
73 74
74 75 See the specification:
75 76 <http://json-rpc.org/wiki/specification>`.
76 77
77 78 Valid controller return values should be json-serializable objects.
78 79
79 80 Sub-classes should catch their exceptions and raise JSONRPCError
80 81 if they want to pass meaningful errors to the client.
81 82
82 83 """
83 84
84 85 def _get_method_args(self):
85 86 """
86 87 Return `self._rpc_args` to dispatched controller method
87 88 chosen by __call__
88 89 """
89 90 return self._rpc_args
90 91
91 92 def __call__(self, environ, start_response):
92 93 """
93 94 Parse the request body as JSON, look up the method on the
94 95 controller and if it exists, dispatch to it.
95 96 """
96 97 if 'CONTENT_LENGTH' not in environ:
97 98 log.debug("No Content-Length")
98 99 return jsonrpc_error(message="No Content-Length in request")
99 100 else:
100 101 length = environ['CONTENT_LENGTH'] or 0
101 102 length = int(environ['CONTENT_LENGTH'])
102 103 log.debug('Content-Length: %s', length)
103 104
104 105 if length == 0:
105 106 log.debug("Content-Length is 0")
106 107 return jsonrpc_error(message="Content-Length is 0")
107 108
108 109 raw_body = environ['wsgi.input'].read(length)
109 110
110 111 try:
111 112 json_body = json.loads(urllib.unquote_plus(raw_body))
112 113 except ValueError, e:
113 114 #catch JSON errors Here
114 115 return jsonrpc_error(message="JSON parse error ERR:%s RAW:%r" \
115 116 % (e, urllib.unquote_plus(raw_body)))
116 117
117 118 # check AUTH based on API KEY
118 119 try:
119 120 self._req_api_key = json_body['api_key']
121 self._req_id = json_body['id']
120 122 self._req_method = json_body['method']
121 123 self._req_params = json_body['args']
122 124 log.debug('method: %s, params: %s',
123 125 self._req_method,
124 126 self._req_params)
125 127 except KeyError, e:
126 128 return jsonrpc_error(message='Incorrect JSON query missing %s' % e)
127 129
128 130 # check if we can find this session using api_key
129 131 try:
130 132 u = User.get_by_api_key(self._req_api_key)
131 133 if u is None:
132 134 return jsonrpc_error(message='Invalid API KEY')
133 135 auth_u = AuthUser(u.user_id, self._req_api_key)
134 136 except Exception, e:
135 137 return jsonrpc_error(message='Invalid API KEY')
136 138
137 139 self._error = None
138 140 try:
139 141 self._func = self._find_method()
140 142 except AttributeError, e:
141 143 return jsonrpc_error(message=str(e))
142 144
143 145 # now that we have a method, add self._req_params to
144 146 # self.kargs and dispatch control to WGIController
145 147 argspec = inspect.getargspec(self._func)
146 148 arglist = argspec[0][1:]
147 149 defaults = argspec[3] or []
148 150 default_empty = types.NotImplementedType
149 151
150 152 kwarglist = list(izip_longest(reversed(arglist), reversed(defaults),
151 153 fillvalue=default_empty))
152 154
153 155 # this is little trick to inject logged in user for
154 156 # perms decorators to work they expect the controller class to have
155 157 # rhodecode_user attribute set
156 158 self.rhodecode_user = auth_u
157 159
158 160 # This attribute will need to be first param of a method that uses
159 161 # api_key, which is translated to instance of user at that name
160 162 USER_SESSION_ATTR = 'apiuser'
161 163
162 164 if USER_SESSION_ATTR not in arglist:
163 165 return jsonrpc_error(message='This method [%s] does not support '
164 166 'authentication (missing %s param)' %
165 167 (self._func.__name__, USER_SESSION_ATTR))
166 168
167 169 # get our arglist and check if we provided them as args
168 170 for arg, default in kwarglist:
169 171 if arg == USER_SESSION_ATTR:
170 172 # USER_SESSION_ATTR is something translated from api key and
171 173 # this is checked before so we don't need validate it
172 174 continue
173 175
174 176 # skip the required param check if it's default value is
175 177 # NotImplementedType (default_empty)
176 178 if not self._req_params or (type(default) == default_empty
177 179 and arg not in self._req_params):
178 180 return jsonrpc_error(message=('Missing non optional %s arg '
179 181 'in JSON DATA') % arg)
180 182
181 183 self._rpc_args = {USER_SESSION_ATTR:u}
182 184 self._rpc_args.update(self._req_params)
183 185
184 186 self._rpc_args['action'] = self._req_method
185 187 self._rpc_args['environ'] = environ
186 188 self._rpc_args['start_response'] = start_response
187 189
188 190 status = []
189 191 headers = []
190 192 exc_info = []
191 193 def change_content(new_status, new_headers, new_exc_info=None):
192 194 status.append(new_status)
193 195 headers.extend(new_headers)
194 196 exc_info.append(new_exc_info)
195 197
196 198 output = WSGIController.__call__(self, environ, change_content)
197 199 output = list(output)
198 200 headers.append(('Content-Length', str(len(output[0]))))
199 201 replace_header(headers, 'Content-Type', 'application/json')
200 202 start_response(status[0], headers, exc_info[0])
201 203
202 204 return output
203 205
204 206 def _dispatch_call(self):
205 207 """
206 208 Implement dispatch interface specified by WSGIController
207 209 """
208 210 try:
209 211 raw_response = self._inspect_call(self._func)
210 212 if isinstance(raw_response, HTTPError):
211 213 self._error = str(raw_response)
212 214 except JSONRPCError, e:
213 215 self._error = str(e)
214 216 except Exception, e:
215 217 log.error('Encountered unhandled exception: %s' \
216 218 % traceback.format_exc())
217 219 json_exc = JSONRPCError('Internal server error')
218 220 self._error = str(json_exc)
219 221
220 222 if self._error is not None:
221 223 raw_response = None
222 224
223 response = dict(result=raw_response, error=self._error)
225 response = dict(id=self._req_id, result=raw_response,
226 error=self._error)
224 227
225 228 try:
226 229 return json.dumps(response)
227 230 except TypeError, e:
228 231 log.debug('Error encoding response: %s', e)
229 232 return json.dumps(dict(result=None,
230 233 error="Error encoding response"))
231 234
232 235 def _find_method(self):
233 236 """
234 237 Return method named by `self._req_method` in controller if able
235 238 """
236 239 log.debug('Trying to find JSON-RPC method: %s', self._req_method)
237 240 if self._req_method.startswith('_'):
238 241 raise AttributeError("Method not allowed")
239 242
240 243 try:
241 244 func = getattr(self, self._req_method, None)
242 245 except UnicodeEncodeError:
243 246 raise AttributeError("Problem decoding unicode in requested "
244 247 "method name.")
245 248
246 249 if isinstance(func, types.MethodType):
247 250 return func
248 251 else:
249 252 raise AttributeError("No such method: %s" % self._req_method)
250 253
General Comments 0
You need to be logged in to leave comments. Login now