blob: ead6ec52447ea249fbf78d7295088c26349658b2 [file] [log] [blame]
Brad Bishop68caa1e2016-03-04 15:42:08 -05001# Contributors Listed Below - COPYRIGHT 2016
2# [+] International Business Machines Corp.
3#
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14# implied. See the License for the specific language governing
15# permissions and limitations under the License.
16
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050017import os
Brad Bishopaa65f6e2015-10-27 16:28:51 -040018import dbus
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050019import dbus.exceptions
20import json
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050021from xml.etree import ElementTree
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050022from bottle import Bottle, abort, request, response, JSONPlugin, HTTPError
Brad Bishopb103d2d2016-03-04 16:19:14 -050023import obmc.utils.misc
Brad Bishopb103d2d2016-03-04 16:19:14 -050024from obmc.dbuslib.introspection import IntrospectionNodeParser
25import obmc.mapper
Brad Bishop2f428582015-12-02 10:56:11 -050026import spwd
27import grp
28import crypt
Brad Bishopaa65f6e2015-10-27 16:28:51 -040029
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050030DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.UnknownInterface'
Brad Bishopf4e74982016-04-01 14:53:05 -040031DBUS_UNKNOWN_INTERFACE_ERROR = 'org.freedesktop.DBus.Error.UnknownInterface'
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050032DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod'
33DBUS_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs'
Brad Bishopd4578922015-12-02 11:10:36 -050034DBUS_TYPE_ERROR = 'org.freedesktop.DBus.Python.TypeError'
Brad Bishopaac521c2015-11-25 09:16:35 -050035DELETE_IFACE = 'org.openbmc.Object.Delete'
Brad Bishop9ee57c42015-11-03 14:59:29 -050036
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050037_4034_msg = "The specified %s cannot be %s: '%s'"
Brad Bishopaa65f6e2015-10-27 16:28:51 -040038
Brad Bishop87b63c12016-03-18 14:47:51 -040039
Brad Bishop2f428582015-12-02 10:56:11 -050040def valid_user(session, *a, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -040041 ''' Authorization plugin callback that checks
42 that the user is logged in. '''
43 if session is None:
Brad Bishopdc3fbfa2016-09-08 09:51:38 -040044 abort(401, 'Login required')
Brad Bishop87b63c12016-03-18 14:47:51 -040045
Brad Bishop2f428582015-12-02 10:56:11 -050046
47class UserInGroup:
Brad Bishop87b63c12016-03-18 14:47:51 -040048 ''' Authorization plugin callback that checks that the user is logged in
49 and a member of a group. '''
50 def __init__(self, group):
51 self.group = group
Brad Bishop2f428582015-12-02 10:56:11 -050052
Brad Bishop87b63c12016-03-18 14:47:51 -040053 def __call__(self, session, *a, **kw):
54 valid_user(session, *a, **kw)
55 res = False
Brad Bishop2f428582015-12-02 10:56:11 -050056
Brad Bishop87b63c12016-03-18 14:47:51 -040057 try:
58 res = session['user'] in grp.getgrnam(self.group)[3]
59 except KeyError:
60 pass
Brad Bishop2f428582015-12-02 10:56:11 -050061
Brad Bishop87b63c12016-03-18 14:47:51 -040062 if not res:
63 abort(403, 'Insufficient access')
64
Brad Bishop2f428582015-12-02 10:56:11 -050065
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050066class RouteHandler(object):
Brad Bishop6d190602016-04-15 13:09:39 -040067 _require_auth = obmc.utils.misc.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
Brad Bishopb103d2d2016-03-04 16:19:14 -050072 self.mapper = obmc.mapper.Mapper(bus)
Brad Bishop6d190602016-04-15 13:09:39 -040073 self._verbs = obmc.utils.misc.makelist(verbs)
Brad Bishop87b63c12016-03-18 14:47:51 -040074 self._rules = rules
Brad Bishop0f79e522016-03-18 13:33:17 -040075 self.intf_match = obmc.utils.misc.org_dot_openbmc_match
Brad Bishopaa65f6e2015-10-27 16:28:51 -040076
Brad Bishop88c76a42017-02-21 00:02:02 -050077 if 'GET' in self._verbs:
78 self._verbs = list(set(self._verbs + ['HEAD']))
Brad Bishopd4c1c552017-02-21 00:07:28 -050079 if 'OPTIONS' not in self._verbs:
80 self._verbs.append('OPTIONS')
Brad Bishop88c76a42017-02-21 00:02:02 -050081
Brad Bishop87b63c12016-03-18 14:47:51 -040082 def _setup(self, **kw):
83 request.route_data = {}
Brad Bishopd4c1c552017-02-21 00:07:28 -050084
Brad Bishop87b63c12016-03-18 14:47:51 -040085 if request.method in self._verbs:
Brad Bishopd4c1c552017-02-21 00:07:28 -050086 if request.method != 'OPTIONS':
87 return self.setup(**kw)
Brad Bishop88c76a42017-02-21 00:02:02 -050088
Brad Bishopd4c1c552017-02-21 00:07:28 -050089 # Javascript implementations will not send credentials
90 # with an OPTIONS request. Don't help malicious clients
91 # by checking the path here and returning a 404 if the
92 # path doesn't exist.
93 return None
Brad Bishop88c76a42017-02-21 00:02:02 -050094
Brad Bishopd4c1c552017-02-21 00:07:28 -050095 # Return 405
Brad Bishop88c76a42017-02-21 00:02:02 -050096 raise HTTPError(
97 405, "Method not allowed.", Allow=','.join(self._verbs))
Brad Bishopaa65f6e2015-10-27 16:28:51 -040098
Brad Bishop87b63c12016-03-18 14:47:51 -040099 def __call__(self, **kw):
100 return getattr(self, 'do_' + request.method.lower())(**kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400101
Brad Bishop88c76a42017-02-21 00:02:02 -0500102 def do_head(self, **kw):
103 return self.do_get(**kw)
104
Brad Bishopd4c1c552017-02-21 00:07:28 -0500105 def do_options(self, **kw):
106 for v in self._verbs:
107 response.set_header(
108 'Allow',
109 ','.join(self._verbs))
110 return None
111
Brad Bishop87b63c12016-03-18 14:47:51 -0400112 def install(self):
113 self.app.route(
114 self._rules, callback=self,
Brad Bishopd4c1c552017-02-21 00:07:28 -0500115 method=['OPTIONS', 'GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400116
Brad Bishop87b63c12016-03-18 14:47:51 -0400117 @staticmethod
118 def try_mapper_call(f, callback=None, **kw):
119 try:
120 return f(**kw)
121 except dbus.exceptions.DBusException, e:
Brad Bishopfce77562016-11-28 15:44:18 -0500122 if e.get_dbus_name() == \
123 'org.freedesktop.DBus.Error.ObjectPathInUse':
124 abort(503, str(e))
Brad Bishopb103d2d2016-03-04 16:19:14 -0500125 if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
Brad Bishop87b63c12016-03-18 14:47:51 -0400126 raise
127 if callback is None:
128 def callback(e, **kw):
129 abort(404, str(e))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400130
Brad Bishop87b63c12016-03-18 14:47:51 -0400131 callback(e, **kw)
132
133 @staticmethod
134 def try_properties_interface(f, *a):
135 try:
136 return f(*a)
137 except dbus.exceptions.DBusException, e:
138 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_message():
139 # interface doesn't have any properties
140 return None
Brad Bishopf4e74982016-04-01 14:53:05 -0400141 if DBUS_UNKNOWN_INTERFACE_ERROR in e.get_dbus_name():
142 # interface doesn't have any properties
143 return None
Brad Bishop87b63c12016-03-18 14:47:51 -0400144 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
145 # properties interface not implemented at all
146 return None
147 raise
148
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400149
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500150class DirectoryHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400151 verbs = 'GET'
152 rules = '<path:path>/'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400153
Brad Bishop87b63c12016-03-18 14:47:51 -0400154 def __init__(self, app, bus):
155 super(DirectoryHandler, self).__init__(
156 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400157
Brad Bishop87b63c12016-03-18 14:47:51 -0400158 def find(self, path='/'):
159 return self.try_mapper_call(
160 self.mapper.get_subtree_paths, path=path, depth=1)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400161
Brad Bishop87b63c12016-03-18 14:47:51 -0400162 def setup(self, path='/'):
163 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400164
Brad Bishop87b63c12016-03-18 14:47:51 -0400165 def do_get(self, path='/'):
166 return request.route_data['map']
167
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400168
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500169class ListNamesHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400170 verbs = 'GET'
171 rules = ['/list', '<path:path>/list']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400172
Brad Bishop87b63c12016-03-18 14:47:51 -0400173 def __init__(self, app, bus):
174 super(ListNamesHandler, self).__init__(
175 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400176
Brad Bishop87b63c12016-03-18 14:47:51 -0400177 def find(self, path='/'):
178 return self.try_mapper_call(
179 self.mapper.get_subtree, path=path).keys()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400180
Brad Bishop87b63c12016-03-18 14:47:51 -0400181 def setup(self, path='/'):
182 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400183
Brad Bishop87b63c12016-03-18 14:47:51 -0400184 def do_get(self, path='/'):
185 return request.route_data['map']
186
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400187
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500188class ListHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400189 verbs = 'GET'
190 rules = ['/enumerate', '<path:path>/enumerate']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400191
Brad Bishop87b63c12016-03-18 14:47:51 -0400192 def __init__(self, app, bus):
193 super(ListHandler, self).__init__(
194 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400195
Brad Bishop87b63c12016-03-18 14:47:51 -0400196 def find(self, path='/'):
197 return self.try_mapper_call(
198 self.mapper.get_subtree, path=path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400199
Brad Bishop87b63c12016-03-18 14:47:51 -0400200 def setup(self, path='/'):
201 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400202
Brad Bishop87b63c12016-03-18 14:47:51 -0400203 def do_get(self, path='/'):
Brad Bishop71527b42016-04-01 14:51:14 -0400204 return {x: y for x, y in self.mapper.enumerate_subtree(
205 path,
206 mapper_data=request.route_data['map']).dataitems()}
Brad Bishop87b63c12016-03-18 14:47:51 -0400207
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400208
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500209class MethodHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400210 verbs = 'POST'
211 rules = '<path:path>/action/<method>'
212 request_type = list
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400213
Brad Bishop87b63c12016-03-18 14:47:51 -0400214 def __init__(self, app, bus):
215 super(MethodHandler, self).__init__(
216 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400217
Brad Bishop87b63c12016-03-18 14:47:51 -0400218 def find(self, path, method):
219 busses = self.try_mapper_call(
220 self.mapper.get_object, path=path)
221 for items in busses.iteritems():
222 m = self.find_method_on_bus(path, method, *items)
223 if m:
224 return m
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400225
Brad Bishop87b63c12016-03-18 14:47:51 -0400226 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400227
Brad Bishop87b63c12016-03-18 14:47:51 -0400228 def setup(self, path, method):
229 request.route_data['method'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400230
Brad Bishop87b63c12016-03-18 14:47:51 -0400231 def do_post(self, path, method):
232 try:
233 if request.parameter_list:
234 return request.route_data['method'](*request.parameter_list)
235 else:
236 return request.route_data['method']()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400237
Brad Bishop87b63c12016-03-18 14:47:51 -0400238 except dbus.exceptions.DBusException, e:
239 if e.get_dbus_name() == DBUS_INVALID_ARGS:
240 abort(400, str(e))
241 if e.get_dbus_name() == DBUS_TYPE_ERROR:
242 abort(400, str(e))
243 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400244
Brad Bishop87b63c12016-03-18 14:47:51 -0400245 @staticmethod
246 def find_method_in_interface(method, obj, interface, methods):
247 if methods is None:
248 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400249
Brad Bishop6d190602016-04-15 13:09:39 -0400250 method = obmc.utils.misc.find_case_insensitive(method, methods.keys())
Brad Bishop87b63c12016-03-18 14:47:51 -0400251 if method is not None:
252 iface = dbus.Interface(obj, interface)
253 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400254
Brad Bishop87b63c12016-03-18 14:47:51 -0400255 def find_method_on_bus(self, path, method, bus, interfaces):
256 obj = self.bus.get_object(bus, path, introspect=False)
257 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
258 data = iface.Introspect()
259 parser = IntrospectionNodeParser(
260 ElementTree.fromstring(data),
Brad Bishopb103d2d2016-03-04 16:19:14 -0500261 intf_match=obmc.utils.misc.ListMatch(interfaces))
Brad Bishop87b63c12016-03-18 14:47:51 -0400262 for x, y in parser.get_interfaces().iteritems():
263 m = self.find_method_in_interface(
264 method, obj, x, y.get('method'))
265 if m:
266 return m
267
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400268
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500269class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400270 verbs = ['PUT', 'GET']
271 rules = '<path:path>/attr/<prop>'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400272
Brad Bishop87b63c12016-03-18 14:47:51 -0400273 def __init__(self, app, bus):
274 super(PropertyHandler, self).__init__(
275 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400276
Brad Bishop87b63c12016-03-18 14:47:51 -0400277 def find(self, path, prop):
278 self.app.instance_handler.setup(path)
279 obj = self.app.instance_handler.do_get(path)
280 try:
281 obj[prop]
282 except KeyError, e:
283 if request.method == 'PUT':
284 abort(403, _4034_msg % ('property', 'created', str(e)))
285 else:
286 abort(404, _4034_msg % ('property', 'found', str(e)))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400287
Brad Bishop87b63c12016-03-18 14:47:51 -0400288 return {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500289
Brad Bishop87b63c12016-03-18 14:47:51 -0400290 def setup(self, path, prop):
291 request.route_data['obj'] = self.find(path, prop)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500292
Brad Bishop87b63c12016-03-18 14:47:51 -0400293 def do_get(self, path, prop):
294 return request.route_data['obj'][path][prop]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500295
Brad Bishop87b63c12016-03-18 14:47:51 -0400296 def do_put(self, path, prop, value=None):
297 if value is None:
298 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500299
Brad Bishop87b63c12016-03-18 14:47:51 -0400300 prop, iface, properties_iface = self.get_host_interface(
301 path, prop, request.route_data['map'][path])
302 try:
303 properties_iface.Set(iface, prop, value)
304 except ValueError, e:
305 abort(400, str(e))
306 except dbus.exceptions.DBusException, e:
307 if e.get_dbus_name() == DBUS_INVALID_ARGS:
308 abort(403, str(e))
309 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500310
Brad Bishop87b63c12016-03-18 14:47:51 -0400311 def get_host_interface(self, path, prop, bus_info):
312 for bus, interfaces in bus_info.iteritems():
313 obj = self.bus.get_object(bus, path, introspect=True)
314 properties_iface = dbus.Interface(
315 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500316
Brad Bishop87b63c12016-03-18 14:47:51 -0400317 info = self.get_host_interface_on_bus(
318 path, prop, properties_iface, bus, interfaces)
319 if info is not None:
320 prop, iface = info
321 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500322
Brad Bishop87b63c12016-03-18 14:47:51 -0400323 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
324 for i in interfaces:
325 properties = self.try_properties_interface(iface.GetAll, i)
326 if properties is None:
327 continue
Brad Bishop8b0d3fa2016-11-28 15:41:47 -0500328 prop = obmc.utils.misc.find_case_insensitive(
329 prop, properties.keys())
Brad Bishop87b63c12016-03-18 14:47:51 -0400330 if prop is None:
331 continue
332 return prop, i
333
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500334
Brad Bishop2503bd62015-12-16 17:56:12 -0500335class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400336 verbs = ['GET']
337 rules = '<path:path>/schema'
Brad Bishop2503bd62015-12-16 17:56:12 -0500338
Brad Bishop87b63c12016-03-18 14:47:51 -0400339 def __init__(self, app, bus):
340 super(SchemaHandler, self).__init__(
341 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500342
Brad Bishop87b63c12016-03-18 14:47:51 -0400343 def find(self, path):
344 return self.try_mapper_call(
345 self.mapper.get_object,
346 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500347
Brad Bishop87b63c12016-03-18 14:47:51 -0400348 def setup(self, path):
349 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500350
Brad Bishop87b63c12016-03-18 14:47:51 -0400351 def do_get(self, path):
352 schema = {}
353 for x in request.route_data['map'].iterkeys():
354 obj = self.bus.get_object(x, path, introspect=False)
355 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
356 data = iface.Introspect()
357 parser = IntrospectionNodeParser(
358 ElementTree.fromstring(data))
359 for x, y in parser.get_interfaces().iteritems():
360 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500361
Brad Bishop87b63c12016-03-18 14:47:51 -0400362 return schema
363
Brad Bishop2503bd62015-12-16 17:56:12 -0500364
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500365class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400366 verbs = ['GET', 'PUT', 'DELETE']
367 rules = '<path:path>'
368 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500369
Brad Bishop87b63c12016-03-18 14:47:51 -0400370 def __init__(self, app, bus):
371 super(InstanceHandler, self).__init__(
372 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500373
Brad Bishop87b63c12016-03-18 14:47:51 -0400374 def find(self, path, callback=None):
375 return {path: self.try_mapper_call(
376 self.mapper.get_object,
377 callback,
378 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500379
Brad Bishop87b63c12016-03-18 14:47:51 -0400380 def setup(self, path):
381 callback = None
382 if request.method == 'PUT':
383 def callback(e, **kw):
384 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500385
Brad Bishop87b63c12016-03-18 14:47:51 -0400386 if request.route_data.get('map') is None:
387 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500388
Brad Bishop87b63c12016-03-18 14:47:51 -0400389 def do_get(self, path):
Brad Bishop71527b42016-04-01 14:51:14 -0400390 return self.mapper.enumerate_object(
391 path,
392 mapper_data=request.route_data['map'])
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500393
Brad Bishop87b63c12016-03-18 14:47:51 -0400394 def do_put(self, path):
395 # make sure all properties exist in the request
396 obj = set(self.do_get(path).keys())
397 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500398
Brad Bishop87b63c12016-03-18 14:47:51 -0400399 diff = list(obj.difference(req))
400 if diff:
401 abort(403, _4034_msg % (
402 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500403
Brad Bishop87b63c12016-03-18 14:47:51 -0400404 diff = list(req.difference(obj))
405 if diff:
406 abort(403, _4034_msg % (
407 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500408
Brad Bishop87b63c12016-03-18 14:47:51 -0400409 for p, v in request.parameter_list.iteritems():
410 self.app.property_handler.do_put(
411 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500412
Brad Bishop87b63c12016-03-18 14:47:51 -0400413 def do_delete(self, path):
414 for bus_info in request.route_data['map'][path].iteritems():
415 if self.bus_missing_delete(path, *bus_info):
416 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500417
Brad Bishop87b63c12016-03-18 14:47:51 -0400418 for bus in request.route_data['map'][path].iterkeys():
419 self.delete_on_bus(path, bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500420
Brad Bishop87b63c12016-03-18 14:47:51 -0400421 def bus_missing_delete(self, path, bus, interfaces):
422 return DELETE_IFACE not in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500423
Brad Bishop87b63c12016-03-18 14:47:51 -0400424 def delete_on_bus(self, path, bus):
425 obj = self.bus.get_object(bus, path, introspect=False)
426 delete_iface = dbus.Interface(
427 obj, dbus_interface=DELETE_IFACE)
428 delete_iface.Delete()
429
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500430
Brad Bishop2f428582015-12-02 10:56:11 -0500431class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400432 ''' Handles the /login and /logout routes, manages
433 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500434
Brad Bishop87b63c12016-03-18 14:47:51 -0400435 rules = ['/login', '/logout']
436 login_str = "User '%s' logged %s"
437 bad_passwd_str = "Invalid username or password"
438 no_user_str = "No user logged in"
439 bad_json_str = "Expecting request format { 'data': " \
440 "[<username>, <password>] }, got '%s'"
441 _require_auth = None
442 MAX_SESSIONS = 16
Brad Bishop2f428582015-12-02 10:56:11 -0500443
Brad Bishop87b63c12016-03-18 14:47:51 -0400444 def __init__(self, app, bus):
445 super(SessionHandler, self).__init__(
446 app, bus)
447 self.hmac_key = os.urandom(128)
448 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500449
Brad Bishop87b63c12016-03-18 14:47:51 -0400450 @staticmethod
451 def authenticate(username, clear):
452 try:
453 encoded = spwd.getspnam(username)[1]
454 return encoded == crypt.crypt(clear, encoded)
455 except KeyError:
456 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500457
Brad Bishop87b63c12016-03-18 14:47:51 -0400458 def invalidate_session(self, session):
459 try:
460 self.session_store.remove(session)
461 except ValueError:
462 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500463
Brad Bishop87b63c12016-03-18 14:47:51 -0400464 def new_session(self):
465 sid = os.urandom(32)
466 if self.MAX_SESSIONS <= len(self.session_store):
467 self.session_store.pop()
468 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500469
Brad Bishop87b63c12016-03-18 14:47:51 -0400470 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500471
Brad Bishop87b63c12016-03-18 14:47:51 -0400472 def get_session(self, sid):
473 sids = [x['sid'] for x in self.session_store]
474 try:
475 return self.session_store[sids.index(sid)]
476 except ValueError:
477 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500478
Brad Bishop87b63c12016-03-18 14:47:51 -0400479 def get_session_from_cookie(self):
480 return self.get_session(
481 request.get_cookie(
482 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500483
Brad Bishop87b63c12016-03-18 14:47:51 -0400484 def do_post(self, **kw):
485 if request.path == '/login':
486 return self.do_login(**kw)
487 else:
488 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500489
Brad Bishop87b63c12016-03-18 14:47:51 -0400490 def do_logout(self, **kw):
491 session = self.get_session_from_cookie()
492 if session is not None:
493 user = session['user']
494 self.invalidate_session(session)
495 response.delete_cookie('sid')
496 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500497
Brad Bishop87b63c12016-03-18 14:47:51 -0400498 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500499
Brad Bishop87b63c12016-03-18 14:47:51 -0400500 def do_login(self, **kw):
501 session = self.get_session_from_cookie()
502 if session is not None:
503 return self.login_str % (session['user'], 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500504
Brad Bishop87b63c12016-03-18 14:47:51 -0400505 if len(request.parameter_list) != 2:
506 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500507
Brad Bishop87b63c12016-03-18 14:47:51 -0400508 if not self.authenticate(*request.parameter_list):
Brad Bishopdc3fbfa2016-09-08 09:51:38 -0400509 abort(401, self.bad_passwd_str)
Brad Bishop2f428582015-12-02 10:56:11 -0500510
Brad Bishop87b63c12016-03-18 14:47:51 -0400511 user = request.parameter_list[0]
512 session = self.new_session()
513 session['user'] = user
514 response.set_cookie(
515 'sid', session['sid'], secret=self.hmac_key,
516 secure=True,
517 httponly=True)
518 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500519
Brad Bishop87b63c12016-03-18 14:47:51 -0400520 def find(self, **kw):
521 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500522
Brad Bishop87b63c12016-03-18 14:47:51 -0400523 def setup(self, **kw):
524 pass
525
Brad Bishop2f428582015-12-02 10:56:11 -0500526
527class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400528 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500529
Brad Bishop87b63c12016-03-18 14:47:51 -0400530 name = 'authorization'
531 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -0500532
Brad Bishop87b63c12016-03-18 14:47:51 -0400533 class Compose:
534 def __init__(self, validators, callback, session_mgr):
535 self.validators = validators
536 self.callback = callback
537 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -0500538
Brad Bishop87b63c12016-03-18 14:47:51 -0400539 def __call__(self, *a, **kw):
540 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
541 session = self.session_mgr.get_session(sid)
Brad Bishopd4c1c552017-02-21 00:07:28 -0500542 if request.method != 'OPTIONS':
543 for x in self.validators:
544 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500545
Brad Bishop87b63c12016-03-18 14:47:51 -0400546 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500547
Brad Bishop87b63c12016-03-18 14:47:51 -0400548 def apply(self, callback, route):
549 undecorated = route.get_undecorated_callback()
550 if not isinstance(undecorated, RouteHandler):
551 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500552
Brad Bishop87b63c12016-03-18 14:47:51 -0400553 auth_types = getattr(
554 undecorated, '_require_auth', None)
555 if not auth_types:
556 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500557
Brad Bishop87b63c12016-03-18 14:47:51 -0400558 return self.Compose(
559 auth_types, callback, undecorated.app.session_handler)
560
Brad Bishop2f428582015-12-02 10:56:11 -0500561
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500562class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400563 ''' Ensures request content satisfies the OpenBMC json api format. '''
564 name = 'json_api_request'
565 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500566
Brad Bishop87b63c12016-03-18 14:47:51 -0400567 error_str = "Expecting request format { 'data': <value> }, got '%s'"
568 type_error_str = "Unsupported Content-Type: '%s'"
569 json_type = "application/json"
570 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500571
Brad Bishop87b63c12016-03-18 14:47:51 -0400572 @staticmethod
573 def content_expected():
574 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500575
Brad Bishop87b63c12016-03-18 14:47:51 -0400576 def validate_request(self):
577 if request.content_length > 0 and \
578 request.content_type != self.json_type:
579 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500580
Brad Bishop87b63c12016-03-18 14:47:51 -0400581 try:
582 request.parameter_list = request.json.get('data')
583 except ValueError, e:
584 abort(400, str(e))
585 except (AttributeError, KeyError, TypeError):
586 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500587
Brad Bishop87b63c12016-03-18 14:47:51 -0400588 def apply(self, callback, route):
589 verbs = getattr(
590 route.get_undecorated_callback(), '_verbs', None)
591 if verbs is None:
592 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500593
Brad Bishop87b63c12016-03-18 14:47:51 -0400594 if not set(self.request_methods).intersection(verbs):
595 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500596
Brad Bishop87b63c12016-03-18 14:47:51 -0400597 def wrap(*a, **kw):
598 if self.content_expected():
599 self.validate_request()
600 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500601
Brad Bishop87b63c12016-03-18 14:47:51 -0400602 return wrap
603
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500604
605class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400606 ''' Ensures request content type satisfies the OpenBMC json api format. '''
607 name = 'json_api_method_request'
608 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500609
Brad Bishop87b63c12016-03-18 14:47:51 -0400610 error_str = "Expecting request format { 'data': %s }, got '%s'"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500611
Brad Bishop87b63c12016-03-18 14:47:51 -0400612 def apply(self, callback, route):
613 request_type = getattr(
614 route.get_undecorated_callback(), 'request_type', None)
615 if request_type is None:
616 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500617
Brad Bishop87b63c12016-03-18 14:47:51 -0400618 def validate_request():
619 if not isinstance(request.parameter_list, request_type):
620 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500621
Brad Bishop87b63c12016-03-18 14:47:51 -0400622 def wrap(*a, **kw):
623 if JsonApiRequestPlugin.content_expected():
624 validate_request()
625 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500626
Brad Bishop87b63c12016-03-18 14:47:51 -0400627 return wrap
628
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500629
Brad Bishop080a48e2017-02-21 22:34:43 -0500630class JsonErrorsPlugin(JSONPlugin):
631 ''' Extend the Bottle JSONPlugin such that it also encodes error
632 responses. '''
633
634 def __init__(self, app, **kw):
635 super(JsonErrorsPlugin, self).__init__(**kw)
636 self.json_opts = {
637 x: y for x, y in kw.iteritems()
638 if x in ['indent', 'sort_keys']}
639 app.install_error_callback(self.error_callback)
640
641 def error_callback(self, response_object, response_body, **kw):
642 response_body['body'] = json.dumps(response_object, **self.json_opts)
643 response.content_type = 'application/json'
644
645
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500646class JsonApiResponsePlugin(object):
Brad Bishop080a48e2017-02-21 22:34:43 -0500647 ''' Emits responses in the OpenBMC json api format. '''
Brad Bishop87b63c12016-03-18 14:47:51 -0400648 name = 'json_api_response'
649 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500650
Brad Bishopd4c1c552017-02-21 00:07:28 -0500651 @staticmethod
652 def has_body():
653 return request.method not in ['OPTIONS']
654
Brad Bishop080a48e2017-02-21 22:34:43 -0500655 def __init__(self, app):
656 app.install_error_callback(self.error_callback)
657
Brad Bishop87b63c12016-03-18 14:47:51 -0400658 def apply(self, callback, route):
659 def wrap(*a, **kw):
Brad Bishopd4c1c552017-02-21 00:07:28 -0500660 data = callback(*a, **kw)
661 if self.has_body():
662 resp = {'data': data}
663 resp['status'] = 'ok'
664 resp['message'] = response.status_line
665 return resp
Brad Bishop87b63c12016-03-18 14:47:51 -0400666 return wrap
667
Brad Bishop080a48e2017-02-21 22:34:43 -0500668 def error_callback(self, error, response_object, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -0400669 response_object['message'] = error.status_line
Brad Bishop080a48e2017-02-21 22:34:43 -0500670 response_object.setdefault('data', {})['description'] = str(error.body)
Brad Bishop87b63c12016-03-18 14:47:51 -0400671 if error.status_code == 500:
672 response_object['data']['exception'] = repr(error.exception)
673 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500674
Brad Bishop87b63c12016-03-18 14:47:51 -0400675
Brad Bishop080a48e2017-02-21 22:34:43 -0500676class JsonpPlugin(object):
Brad Bishop80fe37a2016-03-29 10:54:54 -0400677 ''' Json javascript wrapper. '''
678 name = 'jsonp'
679 api = 2
680
Brad Bishop080a48e2017-02-21 22:34:43 -0500681 def __init__(self, app, **kw):
682 app.install_error_callback(self.error_callback)
Brad Bishop80fe37a2016-03-29 10:54:54 -0400683
684 @staticmethod
685 def to_jsonp(json):
686 jwrapper = request.query.callback or None
687 if(jwrapper):
688 response.set_header('Content-Type', 'application/javascript')
689 json = jwrapper + '(' + json + ');'
690 return json
691
692 def apply(self, callback, route):
693 def wrap(*a, **kw):
694 return self.to_jsonp(callback(*a, **kw))
695 return wrap
696
Brad Bishop080a48e2017-02-21 22:34:43 -0500697 def error_callback(self, response_body, **kw):
698 response_body['body'] = self.to_jsonp(response_body['body'])
Brad Bishop80fe37a2016-03-29 10:54:54 -0400699
700
Brad Bishop2c6fc762016-08-29 15:53:25 -0400701class App(Bottle):
Brad Bishop2ddfa002016-08-29 15:11:55 -0400702 def __init__(self):
Brad Bishop2c6fc762016-08-29 15:53:25 -0400703 super(App, self).__init__(autojson=False)
Brad Bishop2ddfa002016-08-29 15:11:55 -0400704 self.bus = dbus.SystemBus()
705 self.mapper = obmc.mapper.Mapper(self.bus)
Brad Bishop080a48e2017-02-21 22:34:43 -0500706 self.error_callbacks = []
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500707
Brad Bishop87b63c12016-03-18 14:47:51 -0400708 self.install_hooks()
709 self.install_plugins()
710 self.create_handlers()
711 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500712
Brad Bishop87b63c12016-03-18 14:47:51 -0400713 def install_plugins(self):
714 # install json api plugins
715 json_kw = {'indent': 2, 'sort_keys': True}
Brad Bishop87b63c12016-03-18 14:47:51 -0400716 self.install(AuthorizationPlugin())
Brad Bishop080a48e2017-02-21 22:34:43 -0500717 self.install(JsonpPlugin(self, **json_kw))
718 self.install(JsonErrorsPlugin(self, **json_kw))
719 self.install(JsonApiResponsePlugin(self))
Brad Bishop87b63c12016-03-18 14:47:51 -0400720 self.install(JsonApiRequestPlugin())
721 self.install(JsonApiRequestTypePlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500722
Brad Bishop87b63c12016-03-18 14:47:51 -0400723 def install_hooks(self):
Brad Bishop080a48e2017-02-21 22:34:43 -0500724 self.error_handler_type = type(self.default_error_handler)
725 self.original_error_handler = self.default_error_handler
726 self.default_error_handler = self.error_handler_type(
727 self.custom_error_handler, self, Bottle)
728
Brad Bishop87b63c12016-03-18 14:47:51 -0400729 self.real_router_match = self.router.match
730 self.router.match = self.custom_router_match
731 self.add_hook('before_request', self.strip_extra_slashes)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500732
Brad Bishop87b63c12016-03-18 14:47:51 -0400733 def create_handlers(self):
734 # create route handlers
735 self.session_handler = SessionHandler(self, self.bus)
736 self.directory_handler = DirectoryHandler(self, self.bus)
737 self.list_names_handler = ListNamesHandler(self, self.bus)
738 self.list_handler = ListHandler(self, self.bus)
739 self.method_handler = MethodHandler(self, self.bus)
740 self.property_handler = PropertyHandler(self, self.bus)
741 self.schema_handler = SchemaHandler(self, self.bus)
742 self.instance_handler = InstanceHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500743
Brad Bishop87b63c12016-03-18 14:47:51 -0400744 def install_handlers(self):
745 self.session_handler.install()
746 self.directory_handler.install()
747 self.list_names_handler.install()
748 self.list_handler.install()
749 self.method_handler.install()
750 self.property_handler.install()
751 self.schema_handler.install()
752 # this has to come last, since it matches everything
753 self.instance_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500754
Brad Bishop080a48e2017-02-21 22:34:43 -0500755 def install_error_callback(self, callback):
756 self.error_callbacks.insert(0, callback)
757
Brad Bishop87b63c12016-03-18 14:47:51 -0400758 def custom_router_match(self, environ):
759 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
760 needed doesn't work for us since the instance rules match
761 everything. This monkey-patch lets the route handler figure
762 out which response is needed. This could be accomplished
763 with a hook but that would require calling the router match
764 function twice.
765 '''
766 route, args = self.real_router_match(environ)
767 if isinstance(route.callback, RouteHandler):
768 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500769
Brad Bishop87b63c12016-03-18 14:47:51 -0400770 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500771
Brad Bishop080a48e2017-02-21 22:34:43 -0500772 def custom_error_handler(self, res, error):
773 ''' Allow plugins to modify error reponses too via this custom
774 error handler. '''
775
776 response_object = {}
777 response_body = {}
778 for x in self.error_callbacks:
779 x(error=error,
780 response_object=response_object,
781 response_body=response_body)
782
783 return response_body.get('body', "")
784
Brad Bishop87b63c12016-03-18 14:47:51 -0400785 @staticmethod
786 def strip_extra_slashes():
787 path = request.environ['PATH_INFO']
788 trailing = ("", "/")[path[-1] == '/']
789 parts = filter(bool, path.split('/'))
790 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing