blob: f2e268bf275dec77e299bd36bd4bd27470af87bf [file] [log] [blame]
Brad Bishopaa65f6e2015-10-27 16:28:51 -04001#!/usr/bin/env python
2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05003import os
4import sys
Brad Bishopaa65f6e2015-10-27 16:28:51 -04005import dbus
Brad Bishopb1cbdaf2015-11-13 21:28:16 -05006import dbus.exceptions
7import json
8import logging
9from xml.etree import ElementTree
10from rocket import Rocket
11from bottle import Bottle, abort, request, response, JSONPlugin, HTTPError
Brad Bishopaa65f6e2015-10-27 16:28:51 -040012import OpenBMCMapper
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050013from OpenBMCMapper import Mapper, PathTree, IntrospectionNodeParser, ListMatch
Brad Bishop2f428582015-12-02 10:56:11 -050014import spwd
15import grp
16import crypt
Brad Bishopaa65f6e2015-10-27 16:28:51 -040017
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050018DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.UnknownInterface'
19DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod'
20DBUS_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs'
Brad Bishopd4578922015-12-02 11:10:36 -050021DBUS_TYPE_ERROR = 'org.freedesktop.DBus.Python.TypeError'
Brad Bishopaac521c2015-11-25 09:16:35 -050022DELETE_IFACE = 'org.openbmc.Object.Delete'
Brad Bishop9ee57c42015-11-03 14:59:29 -050023
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050024_4034_msg = "The specified %s cannot be %s: '%s'"
Brad Bishopaa65f6e2015-10-27 16:28:51 -040025
Brad Bishop87b63c12016-03-18 14:47:51 -040026
Brad Bishop2f428582015-12-02 10:56:11 -050027def valid_user(session, *a, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -040028 ''' Authorization plugin callback that checks
29 that the user is logged in. '''
30 if session is None:
31 abort(403, 'Login required')
32
Brad Bishop2f428582015-12-02 10:56:11 -050033
34class UserInGroup:
Brad Bishop87b63c12016-03-18 14:47:51 -040035 ''' Authorization plugin callback that checks that the user is logged in
36 and a member of a group. '''
37 def __init__(self, group):
38 self.group = group
Brad Bishop2f428582015-12-02 10:56:11 -050039
Brad Bishop87b63c12016-03-18 14:47:51 -040040 def __call__(self, session, *a, **kw):
41 valid_user(session, *a, **kw)
42 res = False
Brad Bishop2f428582015-12-02 10:56:11 -050043
Brad Bishop87b63c12016-03-18 14:47:51 -040044 try:
45 res = session['user'] in grp.getgrnam(self.group)[3]
46 except KeyError:
47 pass
Brad Bishop2f428582015-12-02 10:56:11 -050048
Brad Bishop87b63c12016-03-18 14:47:51 -040049 if not res:
50 abort(403, 'Insufficient access')
51
Brad Bishop2f428582015-12-02 10:56:11 -050052
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050053def find_case_insensitive(value, lst):
Brad Bishop87b63c12016-03-18 14:47:51 -040054 return next((x for x in lst if x.lower() == value.lower()), None)
55
Brad Bishopaa65f6e2015-10-27 16:28:51 -040056
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050057def makelist(data):
Brad Bishop87b63c12016-03-18 14:47:51 -040058 if isinstance(data, list):
59 return data
60 elif data:
61 return [data]
62 else:
63 return []
64
Brad Bishopaa65f6e2015-10-27 16:28:51 -040065
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050066class RouteHandler(object):
Brad Bishop87b63c12016-03-18 14:47:51 -040067 _require_auth = makelist(valid_user)
Brad Bishopaa65f6e2015-10-27 16:28:51 -040068
Brad Bishop87b63c12016-03-18 14:47:51 -040069 def __init__(self, app, bus, verbs, rules):
70 self.app = app
71 self.bus = bus
72 self.mapper = Mapper(bus)
73 self._verbs = makelist(verbs)
74 self._rules = rules
Brad Bishopaa65f6e2015-10-27 16:28:51 -040075
Brad Bishop87b63c12016-03-18 14:47:51 -040076 def _setup(self, **kw):
77 request.route_data = {}
78 if request.method in self._verbs:
79 return self.setup(**kw)
80 else:
81 self.find(**kw)
82 raise HTTPError(
83 405, "Method not allowed.", Allow=','.join(self._verbs))
Brad Bishopaa65f6e2015-10-27 16:28:51 -040084
Brad Bishop87b63c12016-03-18 14:47:51 -040085 def __call__(self, **kw):
86 return getattr(self, 'do_' + request.method.lower())(**kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -040087
Brad Bishop87b63c12016-03-18 14:47:51 -040088 def install(self):
89 self.app.route(
90 self._rules, callback=self,
91 method=['GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
Brad Bishopaa65f6e2015-10-27 16:28:51 -040092
Brad Bishop87b63c12016-03-18 14:47:51 -040093 @staticmethod
94 def try_mapper_call(f, callback=None, **kw):
95 try:
96 return f(**kw)
97 except dbus.exceptions.DBusException, e:
98 if e.get_dbus_name() != OpenBMCMapper.MAPPER_NOT_FOUND:
99 raise
100 if callback is None:
101 def callback(e, **kw):
102 abort(404, str(e))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400103
Brad Bishop87b63c12016-03-18 14:47:51 -0400104 callback(e, **kw)
105
106 @staticmethod
107 def try_properties_interface(f, *a):
108 try:
109 return f(*a)
110 except dbus.exceptions.DBusException, e:
111 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_message():
112 # interface doesn't have any properties
113 return None
114 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
115 # properties interface not implemented at all
116 return None
117 raise
118
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400119
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500120class DirectoryHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400121 verbs = 'GET'
122 rules = '<path:path>/'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400123
Brad Bishop87b63c12016-03-18 14:47:51 -0400124 def __init__(self, app, bus):
125 super(DirectoryHandler, self).__init__(
126 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400127
Brad Bishop87b63c12016-03-18 14:47:51 -0400128 def find(self, path='/'):
129 return self.try_mapper_call(
130 self.mapper.get_subtree_paths, path=path, depth=1)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400131
Brad Bishop87b63c12016-03-18 14:47:51 -0400132 def setup(self, path='/'):
133 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400134
Brad Bishop87b63c12016-03-18 14:47:51 -0400135 def do_get(self, path='/'):
136 return request.route_data['map']
137
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400138
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500139class ListNamesHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400140 verbs = 'GET'
141 rules = ['/list', '<path:path>/list']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400142
Brad Bishop87b63c12016-03-18 14:47:51 -0400143 def __init__(self, app, bus):
144 super(ListNamesHandler, self).__init__(
145 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400146
Brad Bishop87b63c12016-03-18 14:47:51 -0400147 def find(self, path='/'):
148 return self.try_mapper_call(
149 self.mapper.get_subtree, path=path).keys()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400150
Brad Bishop87b63c12016-03-18 14:47:51 -0400151 def setup(self, path='/'):
152 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400153
Brad Bishop87b63c12016-03-18 14:47:51 -0400154 def do_get(self, path='/'):
155 return request.route_data['map']
156
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400157
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500158class ListHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400159 verbs = 'GET'
160 rules = ['/enumerate', '<path:path>/enumerate']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400161
Brad Bishop87b63c12016-03-18 14:47:51 -0400162 def __init__(self, app, bus):
163 super(ListHandler, self).__init__(
164 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400165
Brad Bishop87b63c12016-03-18 14:47:51 -0400166 def find(self, path='/'):
167 return self.try_mapper_call(
168 self.mapper.get_subtree, path=path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400169
Brad Bishop87b63c12016-03-18 14:47:51 -0400170 def setup(self, path='/'):
171 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400172
Brad Bishop87b63c12016-03-18 14:47:51 -0400173 def do_get(self, path='/'):
174 objs = {}
175 mapper_data = request.route_data['map']
176 tree = PathTree()
177 for x, y in mapper_data.iteritems():
178 tree[x] = y
Brad Bishop936f5fe2015-11-03 15:10:11 -0500179
Brad Bishop87b63c12016-03-18 14:47:51 -0400180 try:
181 # Check to see if the root path implements
182 # enumerate in addition to any sub tree
183 # objects.
184 root = self.try_mapper_call(
185 self.mapper.get_object, path=path)
186 mapper_data[path] = root
187 except:
188 pass
Brad Bishop936f5fe2015-11-03 15:10:11 -0500189
Brad Bishop87b63c12016-03-18 14:47:51 -0400190 have_enumerate = [
191 (x[0], self.enumerate_capable(*x))
192 for x in mapper_data.iteritems() if self.enumerate_capable(*x)]
Brad Bishop936f5fe2015-11-03 15:10:11 -0500193
Brad Bishop87b63c12016-03-18 14:47:51 -0400194 for x, y in have_enumerate:
195 objs.update(self.call_enumerate(x, y))
196 tmp = tree[x]
197 # remove the subtree
198 del tree[x]
199 # add the new leaf back since enumerate results don't
200 # include the object enumerate is being invoked on
201 tree[x] = tmp
Brad Bishop936f5fe2015-11-03 15:10:11 -0500202
Brad Bishop87b63c12016-03-18 14:47:51 -0400203 # make dbus calls for any remaining objects
204 for x, y in tree.dataitems():
205 objs[x] = self.app.instance_handler.do_get(x)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400206
Brad Bishop87b63c12016-03-18 14:47:51 -0400207 return objs
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400208
Brad Bishop87b63c12016-03-18 14:47:51 -0400209 @staticmethod
210 def enumerate_capable(path, bus_data):
211 busses = []
212 for name, ifaces in bus_data.iteritems():
213 if OpenBMCMapper.ENUMERATE_IFACE in ifaces:
214 busses.append(name)
215 return busses
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400216
Brad Bishop87b63c12016-03-18 14:47:51 -0400217 def call_enumerate(self, path, busses):
218 objs = {}
219 for b in busses:
220 obj = self.bus.get_object(b, path, introspect=False)
221 iface = dbus.Interface(obj, OpenBMCMapper.ENUMERATE_IFACE)
222 objs.update(iface.enumerate())
223 return objs
224
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400225
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500226class MethodHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400227 verbs = 'POST'
228 rules = '<path:path>/action/<method>'
229 request_type = list
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400230
Brad Bishop87b63c12016-03-18 14:47:51 -0400231 def __init__(self, app, bus):
232 super(MethodHandler, self).__init__(
233 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400234
Brad Bishop87b63c12016-03-18 14:47:51 -0400235 def find(self, path, method):
236 busses = self.try_mapper_call(
237 self.mapper.get_object, path=path)
238 for items in busses.iteritems():
239 m = self.find_method_on_bus(path, method, *items)
240 if m:
241 return m
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400242
Brad Bishop87b63c12016-03-18 14:47:51 -0400243 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400244
Brad Bishop87b63c12016-03-18 14:47:51 -0400245 def setup(self, path, method):
246 request.route_data['method'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400247
Brad Bishop87b63c12016-03-18 14:47:51 -0400248 def do_post(self, path, method):
249 try:
250 if request.parameter_list:
251 return request.route_data['method'](*request.parameter_list)
252 else:
253 return request.route_data['method']()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400254
Brad Bishop87b63c12016-03-18 14:47:51 -0400255 except dbus.exceptions.DBusException, e:
256 if e.get_dbus_name() == DBUS_INVALID_ARGS:
257 abort(400, str(e))
258 if e.get_dbus_name() == DBUS_TYPE_ERROR:
259 abort(400, str(e))
260 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400261
Brad Bishop87b63c12016-03-18 14:47:51 -0400262 @staticmethod
263 def find_method_in_interface(method, obj, interface, methods):
264 if methods is None:
265 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400266
Brad Bishop87b63c12016-03-18 14:47:51 -0400267 method = find_case_insensitive(method, methods.keys())
268 if method is not None:
269 iface = dbus.Interface(obj, interface)
270 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400271
Brad Bishop87b63c12016-03-18 14:47:51 -0400272 def find_method_on_bus(self, path, method, bus, interfaces):
273 obj = self.bus.get_object(bus, path, introspect=False)
274 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
275 data = iface.Introspect()
276 parser = IntrospectionNodeParser(
277 ElementTree.fromstring(data),
278 intf_match=ListMatch(interfaces))
279 for x, y in parser.get_interfaces().iteritems():
280 m = self.find_method_in_interface(
281 method, obj, x, y.get('method'))
282 if m:
283 return m
284
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400285
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500286class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400287 verbs = ['PUT', 'GET']
288 rules = '<path:path>/attr/<prop>'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400289
Brad Bishop87b63c12016-03-18 14:47:51 -0400290 def __init__(self, app, bus):
291 super(PropertyHandler, self).__init__(
292 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400293
Brad Bishop87b63c12016-03-18 14:47:51 -0400294 def find(self, path, prop):
295 self.app.instance_handler.setup(path)
296 obj = self.app.instance_handler.do_get(path)
297 try:
298 obj[prop]
299 except KeyError, e:
300 if request.method == 'PUT':
301 abort(403, _4034_msg % ('property', 'created', str(e)))
302 else:
303 abort(404, _4034_msg % ('property', 'found', str(e)))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400304
Brad Bishop87b63c12016-03-18 14:47:51 -0400305 return {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500306
Brad Bishop87b63c12016-03-18 14:47:51 -0400307 def setup(self, path, prop):
308 request.route_data['obj'] = self.find(path, prop)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500309
Brad Bishop87b63c12016-03-18 14:47:51 -0400310 def do_get(self, path, prop):
311 return request.route_data['obj'][path][prop]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500312
Brad Bishop87b63c12016-03-18 14:47:51 -0400313 def do_put(self, path, prop, value=None):
314 if value is None:
315 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500316
Brad Bishop87b63c12016-03-18 14:47:51 -0400317 prop, iface, properties_iface = self.get_host_interface(
318 path, prop, request.route_data['map'][path])
319 try:
320 properties_iface.Set(iface, prop, value)
321 except ValueError, e:
322 abort(400, str(e))
323 except dbus.exceptions.DBusException, e:
324 if e.get_dbus_name() == DBUS_INVALID_ARGS:
325 abort(403, str(e))
326 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500327
Brad Bishop87b63c12016-03-18 14:47:51 -0400328 def get_host_interface(self, path, prop, bus_info):
329 for bus, interfaces in bus_info.iteritems():
330 obj = self.bus.get_object(bus, path, introspect=True)
331 properties_iface = dbus.Interface(
332 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500333
Brad Bishop87b63c12016-03-18 14:47:51 -0400334 info = self.get_host_interface_on_bus(
335 path, prop, properties_iface, bus, interfaces)
336 if info is not None:
337 prop, iface = info
338 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500339
Brad Bishop87b63c12016-03-18 14:47:51 -0400340 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
341 for i in interfaces:
342 properties = self.try_properties_interface(iface.GetAll, i)
343 if properties is None:
344 continue
345 prop = find_case_insensitive(prop, properties.keys())
346 if prop is None:
347 continue
348 return prop, i
349
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500350
Brad Bishop2503bd62015-12-16 17:56:12 -0500351class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400352 verbs = ['GET']
353 rules = '<path:path>/schema'
Brad Bishop2503bd62015-12-16 17:56:12 -0500354
Brad Bishop87b63c12016-03-18 14:47:51 -0400355 def __init__(self, app, bus):
356 super(SchemaHandler, self).__init__(
357 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500358
Brad Bishop87b63c12016-03-18 14:47:51 -0400359 def find(self, path):
360 return self.try_mapper_call(
361 self.mapper.get_object,
362 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500363
Brad Bishop87b63c12016-03-18 14:47:51 -0400364 def setup(self, path):
365 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500366
Brad Bishop87b63c12016-03-18 14:47:51 -0400367 def do_get(self, path):
368 schema = {}
369 for x in request.route_data['map'].iterkeys():
370 obj = self.bus.get_object(x, path, introspect=False)
371 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
372 data = iface.Introspect()
373 parser = IntrospectionNodeParser(
374 ElementTree.fromstring(data))
375 for x, y in parser.get_interfaces().iteritems():
376 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500377
Brad Bishop87b63c12016-03-18 14:47:51 -0400378 return schema
379
Brad Bishop2503bd62015-12-16 17:56:12 -0500380
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500381class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400382 verbs = ['GET', 'PUT', 'DELETE']
383 rules = '<path:path>'
384 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500385
Brad Bishop87b63c12016-03-18 14:47:51 -0400386 def __init__(self, app, bus):
387 super(InstanceHandler, self).__init__(
388 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500389
Brad Bishop87b63c12016-03-18 14:47:51 -0400390 def find(self, path, callback=None):
391 return {path: self.try_mapper_call(
392 self.mapper.get_object,
393 callback,
394 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500395
Brad Bishop87b63c12016-03-18 14:47:51 -0400396 def setup(self, path):
397 callback = None
398 if request.method == 'PUT':
399 def callback(e, **kw):
400 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500401
Brad Bishop87b63c12016-03-18 14:47:51 -0400402 if request.route_data.get('map') is None:
403 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500404
Brad Bishop87b63c12016-03-18 14:47:51 -0400405 def do_get(self, path):
406 properties = {}
407 for item in request.route_data['map'][path].iteritems():
408 properties.update(self.get_properties_on_bus(
409 path, *item))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500410
Brad Bishop87b63c12016-03-18 14:47:51 -0400411 return properties
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500412
Brad Bishop87b63c12016-03-18 14:47:51 -0400413 @staticmethod
414 def get_properties_on_iface(properties_iface, iface):
415 properties = InstanceHandler.try_properties_interface(
416 properties_iface.GetAll, iface)
417 if properties is None:
418 return {}
419 return properties
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500420
Brad Bishop87b63c12016-03-18 14:47:51 -0400421 def get_properties_on_bus(self, path, bus, interfaces):
422 properties = {}
423 obj = self.bus.get_object(bus, path, introspect=False)
424 properties_iface = dbus.Interface(
425 obj, dbus_interface=dbus.PROPERTIES_IFACE)
426 for i in interfaces:
427 properties.update(self.get_properties_on_iface(
428 properties_iface, i))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500429
Brad Bishop87b63c12016-03-18 14:47:51 -0400430 return properties
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500431
Brad Bishop87b63c12016-03-18 14:47:51 -0400432 def do_put(self, path):
433 # make sure all properties exist in the request
434 obj = set(self.do_get(path).keys())
435 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500436
Brad Bishop87b63c12016-03-18 14:47:51 -0400437 diff = list(obj.difference(req))
438 if diff:
439 abort(403, _4034_msg % (
440 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500441
Brad Bishop87b63c12016-03-18 14:47:51 -0400442 diff = list(req.difference(obj))
443 if diff:
444 abort(403, _4034_msg % (
445 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500446
Brad Bishop87b63c12016-03-18 14:47:51 -0400447 for p, v in request.parameter_list.iteritems():
448 self.app.property_handler.do_put(
449 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500450
Brad Bishop87b63c12016-03-18 14:47:51 -0400451 def do_delete(self, path):
452 for bus_info in request.route_data['map'][path].iteritems():
453 if self.bus_missing_delete(path, *bus_info):
454 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500455
Brad Bishop87b63c12016-03-18 14:47:51 -0400456 for bus in request.route_data['map'][path].iterkeys():
457 self.delete_on_bus(path, bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500458
Brad Bishop87b63c12016-03-18 14:47:51 -0400459 def bus_missing_delete(self, path, bus, interfaces):
460 return DELETE_IFACE not in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500461
Brad Bishop87b63c12016-03-18 14:47:51 -0400462 def delete_on_bus(self, path, bus):
463 obj = self.bus.get_object(bus, path, introspect=False)
464 delete_iface = dbus.Interface(
465 obj, dbus_interface=DELETE_IFACE)
466 delete_iface.Delete()
467
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500468
Brad Bishop2f428582015-12-02 10:56:11 -0500469class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400470 ''' Handles the /login and /logout routes, manages
471 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500472
Brad Bishop87b63c12016-03-18 14:47:51 -0400473 rules = ['/login', '/logout']
474 login_str = "User '%s' logged %s"
475 bad_passwd_str = "Invalid username or password"
476 no_user_str = "No user logged in"
477 bad_json_str = "Expecting request format { 'data': " \
478 "[<username>, <password>] }, got '%s'"
479 _require_auth = None
480 MAX_SESSIONS = 16
Brad Bishop2f428582015-12-02 10:56:11 -0500481
Brad Bishop87b63c12016-03-18 14:47:51 -0400482 def __init__(self, app, bus):
483 super(SessionHandler, self).__init__(
484 app, bus)
485 self.hmac_key = os.urandom(128)
486 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500487
Brad Bishop87b63c12016-03-18 14:47:51 -0400488 @staticmethod
489 def authenticate(username, clear):
490 try:
491 encoded = spwd.getspnam(username)[1]
492 return encoded == crypt.crypt(clear, encoded)
493 except KeyError:
494 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500495
Brad Bishop87b63c12016-03-18 14:47:51 -0400496 def invalidate_session(self, session):
497 try:
498 self.session_store.remove(session)
499 except ValueError:
500 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500501
Brad Bishop87b63c12016-03-18 14:47:51 -0400502 def new_session(self):
503 sid = os.urandom(32)
504 if self.MAX_SESSIONS <= len(self.session_store):
505 self.session_store.pop()
506 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500507
Brad Bishop87b63c12016-03-18 14:47:51 -0400508 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500509
Brad Bishop87b63c12016-03-18 14:47:51 -0400510 def get_session(self, sid):
511 sids = [x['sid'] for x in self.session_store]
512 try:
513 return self.session_store[sids.index(sid)]
514 except ValueError:
515 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500516
Brad Bishop87b63c12016-03-18 14:47:51 -0400517 def get_session_from_cookie(self):
518 return self.get_session(
519 request.get_cookie(
520 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500521
Brad Bishop87b63c12016-03-18 14:47:51 -0400522 def do_post(self, **kw):
523 if request.path == '/login':
524 return self.do_login(**kw)
525 else:
526 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500527
Brad Bishop87b63c12016-03-18 14:47:51 -0400528 def do_logout(self, **kw):
529 session = self.get_session_from_cookie()
530 if session is not None:
531 user = session['user']
532 self.invalidate_session(session)
533 response.delete_cookie('sid')
534 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500535
Brad Bishop87b63c12016-03-18 14:47:51 -0400536 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500537
Brad Bishop87b63c12016-03-18 14:47:51 -0400538 def do_login(self, **kw):
539 session = self.get_session_from_cookie()
540 if session is not None:
541 return self.login_str % (session['user'], 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500542
Brad Bishop87b63c12016-03-18 14:47:51 -0400543 if len(request.parameter_list) != 2:
544 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500545
Brad Bishop87b63c12016-03-18 14:47:51 -0400546 if not self.authenticate(*request.parameter_list):
547 return self.bad_passwd_str
Brad Bishop2f428582015-12-02 10:56:11 -0500548
Brad Bishop87b63c12016-03-18 14:47:51 -0400549 user = request.parameter_list[0]
550 session = self.new_session()
551 session['user'] = user
552 response.set_cookie(
553 'sid', session['sid'], secret=self.hmac_key,
554 secure=True,
555 httponly=True)
556 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500557
Brad Bishop87b63c12016-03-18 14:47:51 -0400558 def find(self, **kw):
559 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500560
Brad Bishop87b63c12016-03-18 14:47:51 -0400561 def setup(self, **kw):
562 pass
563
Brad Bishop2f428582015-12-02 10:56:11 -0500564
565class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400566 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500567
Brad Bishop87b63c12016-03-18 14:47:51 -0400568 name = 'authorization'
569 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -0500570
Brad Bishop87b63c12016-03-18 14:47:51 -0400571 class Compose:
572 def __init__(self, validators, callback, session_mgr):
573 self.validators = validators
574 self.callback = callback
575 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -0500576
Brad Bishop87b63c12016-03-18 14:47:51 -0400577 def __call__(self, *a, **kw):
578 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
579 session = self.session_mgr.get_session(sid)
580 for x in self.validators:
581 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500582
Brad Bishop87b63c12016-03-18 14:47:51 -0400583 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500584
Brad Bishop87b63c12016-03-18 14:47:51 -0400585 def apply(self, callback, route):
586 undecorated = route.get_undecorated_callback()
587 if not isinstance(undecorated, RouteHandler):
588 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500589
Brad Bishop87b63c12016-03-18 14:47:51 -0400590 auth_types = getattr(
591 undecorated, '_require_auth', None)
592 if not auth_types:
593 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500594
Brad Bishop87b63c12016-03-18 14:47:51 -0400595 return self.Compose(
596 auth_types, callback, undecorated.app.session_handler)
597
Brad Bishop2f428582015-12-02 10:56:11 -0500598
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500599class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400600 ''' Ensures request content satisfies the OpenBMC json api format. '''
601 name = 'json_api_request'
602 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500603
Brad Bishop87b63c12016-03-18 14:47:51 -0400604 error_str = "Expecting request format { 'data': <value> }, got '%s'"
605 type_error_str = "Unsupported Content-Type: '%s'"
606 json_type = "application/json"
607 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500608
Brad Bishop87b63c12016-03-18 14:47:51 -0400609 @staticmethod
610 def content_expected():
611 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500612
Brad Bishop87b63c12016-03-18 14:47:51 -0400613 def validate_request(self):
614 if request.content_length > 0 and \
615 request.content_type != self.json_type:
616 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500617
Brad Bishop87b63c12016-03-18 14:47:51 -0400618 try:
619 request.parameter_list = request.json.get('data')
620 except ValueError, e:
621 abort(400, str(e))
622 except (AttributeError, KeyError, TypeError):
623 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500624
Brad Bishop87b63c12016-03-18 14:47:51 -0400625 def apply(self, callback, route):
626 verbs = getattr(
627 route.get_undecorated_callback(), '_verbs', None)
628 if verbs is None:
629 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500630
Brad Bishop87b63c12016-03-18 14:47:51 -0400631 if not set(self.request_methods).intersection(verbs):
632 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500633
Brad Bishop87b63c12016-03-18 14:47:51 -0400634 def wrap(*a, **kw):
635 if self.content_expected():
636 self.validate_request()
637 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500638
Brad Bishop87b63c12016-03-18 14:47:51 -0400639 return wrap
640
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500641
642class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400643 ''' Ensures request content type satisfies the OpenBMC json api format. '''
644 name = 'json_api_method_request'
645 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500646
Brad Bishop87b63c12016-03-18 14:47:51 -0400647 error_str = "Expecting request format { 'data': %s }, got '%s'"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500648
Brad Bishop87b63c12016-03-18 14:47:51 -0400649 def apply(self, callback, route):
650 request_type = getattr(
651 route.get_undecorated_callback(), 'request_type', None)
652 if request_type is None:
653 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500654
Brad Bishop87b63c12016-03-18 14:47:51 -0400655 def validate_request():
656 if not isinstance(request.parameter_list, request_type):
657 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500658
Brad Bishop87b63c12016-03-18 14:47:51 -0400659 def wrap(*a, **kw):
660 if JsonApiRequestPlugin.content_expected():
661 validate_request()
662 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500663
Brad Bishop87b63c12016-03-18 14:47:51 -0400664 return wrap
665
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500666
667class JsonApiResponsePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400668 ''' Emits normal responses in the OpenBMC json api format. '''
669 name = 'json_api_response'
670 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500671
Brad Bishop87b63c12016-03-18 14:47:51 -0400672 def apply(self, callback, route):
673 def wrap(*a, **kw):
674 resp = {'data': callback(*a, **kw)}
675 resp['status'] = 'ok'
676 resp['message'] = response.status_line
677 return resp
678 return wrap
679
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500680
681class JsonApiErrorsPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400682 ''' Emits error responses in the OpenBMC json api format. '''
683 name = 'json_api_errors'
684 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500685
Brad Bishop87b63c12016-03-18 14:47:51 -0400686 def __init__(self, **kw):
687 self.app = None
688 self.function_type = None
689 self.original = None
690 self.json_opts = {
691 x: y for x, y in kw.iteritems()
692 if x in ['indent', 'sort_keys']}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500693
Brad Bishop87b63c12016-03-18 14:47:51 -0400694 def setup(self, app):
695 self.app = app
696 self.function_type = type(app.default_error_handler)
697 self.original = app.default_error_handler
698 self.app.default_error_handler = self.function_type(
699 self.json_errors, app, Bottle)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500700
Brad Bishop87b63c12016-03-18 14:47:51 -0400701 def apply(self, callback, route):
702 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500703
Brad Bishop87b63c12016-03-18 14:47:51 -0400704 def close(self):
705 self.app.default_error_handler = self.function_type(
706 self.original, self.app, Bottle)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500707
Brad Bishop87b63c12016-03-18 14:47:51 -0400708 def json_errors(self, res, error):
709 response_object = {'status': 'error', 'data': {}}
710 response_object['message'] = error.status_line
711 response_object['data']['description'] = str(error.body)
712 if error.status_code == 500:
713 response_object['data']['exception'] = repr(error.exception)
714 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500715
Brad Bishop87b63c12016-03-18 14:47:51 -0400716 json_response = json.dumps(response_object, **self.json_opts)
717 response.content_type = 'application/json'
718 return json_response
719
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500720
721class RestApp(Bottle):
Brad Bishop87b63c12016-03-18 14:47:51 -0400722 def __init__(self, bus):
723 super(RestApp, self).__init__(autojson=False)
724 self.bus = bus
725 self.mapper = Mapper(bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500726
Brad Bishop87b63c12016-03-18 14:47:51 -0400727 self.install_hooks()
728 self.install_plugins()
729 self.create_handlers()
730 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500731
Brad Bishop87b63c12016-03-18 14:47:51 -0400732 def install_plugins(self):
733 # install json api plugins
734 json_kw = {'indent': 2, 'sort_keys': True}
735 self.install(JSONPlugin(**json_kw))
736 self.install(JsonApiErrorsPlugin(**json_kw))
737 self.install(AuthorizationPlugin())
738 self.install(JsonApiResponsePlugin())
739 self.install(JsonApiRequestPlugin())
740 self.install(JsonApiRequestTypePlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500741
Brad Bishop87b63c12016-03-18 14:47:51 -0400742 def install_hooks(self):
743 self.real_router_match = self.router.match
744 self.router.match = self.custom_router_match
745 self.add_hook('before_request', self.strip_extra_slashes)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500746
Brad Bishop87b63c12016-03-18 14:47:51 -0400747 def create_handlers(self):
748 # create route handlers
749 self.session_handler = SessionHandler(self, self.bus)
750 self.directory_handler = DirectoryHandler(self, self.bus)
751 self.list_names_handler = ListNamesHandler(self, self.bus)
752 self.list_handler = ListHandler(self, self.bus)
753 self.method_handler = MethodHandler(self, self.bus)
754 self.property_handler = PropertyHandler(self, self.bus)
755 self.schema_handler = SchemaHandler(self, self.bus)
756 self.instance_handler = InstanceHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500757
Brad Bishop87b63c12016-03-18 14:47:51 -0400758 def install_handlers(self):
759 self.session_handler.install()
760 self.directory_handler.install()
761 self.list_names_handler.install()
762 self.list_handler.install()
763 self.method_handler.install()
764 self.property_handler.install()
765 self.schema_handler.install()
766 # this has to come last, since it matches everything
767 self.instance_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500768
Brad Bishop87b63c12016-03-18 14:47:51 -0400769 def custom_router_match(self, environ):
770 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
771 needed doesn't work for us since the instance rules match
772 everything. This monkey-patch lets the route handler figure
773 out which response is needed. This could be accomplished
774 with a hook but that would require calling the router match
775 function twice.
776 '''
777 route, args = self.real_router_match(environ)
778 if isinstance(route.callback, RouteHandler):
779 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500780
Brad Bishop87b63c12016-03-18 14:47:51 -0400781 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500782
Brad Bishop87b63c12016-03-18 14:47:51 -0400783 @staticmethod
784 def strip_extra_slashes():
785 path = request.environ['PATH_INFO']
786 trailing = ("", "/")[path[-1] == '/']
787 parts = filter(bool, path.split('/'))
788 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400789
790if __name__ == '__main__':
Brad Bishop87b63c12016-03-18 14:47:51 -0400791 log = logging.getLogger('Rocket.Errors')
792 log.setLevel(logging.INFO)
793 log.addHandler(logging.StreamHandler(sys.stdout))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500794
Brad Bishop87b63c12016-03-18 14:47:51 -0400795 bus = dbus.SystemBus()
796 app = RestApp(bus)
797 default_cert = os.path.join(
798 sys.prefix, 'share', os.path.basename(__file__), 'cert.pem')
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500799
Brad Bishop87b63c12016-03-18 14:47:51 -0400800 server = Rocket(
801 ('0.0.0.0', 443, default_cert, default_cert),
802 'wsgi', {'wsgi_app': app},
803 min_threads=1,
804 max_threads=1)
805 server.start()