blob: aa08bfa8cff43d5a8d35b24571a1681c37c16b92 [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']))
79
Brad Bishop87b63c12016-03-18 14:47:51 -040080 def _setup(self, **kw):
81 request.route_data = {}
82 if request.method in self._verbs:
83 return self.setup(**kw)
Brad Bishop88c76a42017-02-21 00:02:02 -050084
85 # Return 404 if path not found.
86 self.find(**kw)
87
88 # Return 405.
89 raise HTTPError(
90 405, "Method not allowed.", Allow=','.join(self._verbs))
Brad Bishopaa65f6e2015-10-27 16:28:51 -040091
Brad Bishop87b63c12016-03-18 14:47:51 -040092 def __call__(self, **kw):
93 return getattr(self, 'do_' + request.method.lower())(**kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -040094
Brad Bishop88c76a42017-02-21 00:02:02 -050095 def do_head(self, **kw):
96 return self.do_get(**kw)
97
Brad Bishop87b63c12016-03-18 14:47:51 -040098 def install(self):
99 self.app.route(
100 self._rules, callback=self,
101 method=['GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400102
Brad Bishop87b63c12016-03-18 14:47:51 -0400103 @staticmethod
104 def try_mapper_call(f, callback=None, **kw):
105 try:
106 return f(**kw)
107 except dbus.exceptions.DBusException, e:
Brad Bishopfce77562016-11-28 15:44:18 -0500108 if e.get_dbus_name() == \
109 'org.freedesktop.DBus.Error.ObjectPathInUse':
110 abort(503, str(e))
Brad Bishopb103d2d2016-03-04 16:19:14 -0500111 if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
Brad Bishop87b63c12016-03-18 14:47:51 -0400112 raise
113 if callback is None:
114 def callback(e, **kw):
115 abort(404, str(e))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400116
Brad Bishop87b63c12016-03-18 14:47:51 -0400117 callback(e, **kw)
118
119 @staticmethod
120 def try_properties_interface(f, *a):
121 try:
122 return f(*a)
123 except dbus.exceptions.DBusException, e:
124 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_message():
125 # interface doesn't have any properties
126 return None
Brad Bishopf4e74982016-04-01 14:53:05 -0400127 if DBUS_UNKNOWN_INTERFACE_ERROR in e.get_dbus_name():
128 # interface doesn't have any properties
129 return None
Brad Bishop87b63c12016-03-18 14:47:51 -0400130 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
131 # properties interface not implemented at all
132 return None
133 raise
134
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400135
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500136class DirectoryHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400137 verbs = 'GET'
138 rules = '<path:path>/'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400139
Brad Bishop87b63c12016-03-18 14:47:51 -0400140 def __init__(self, app, bus):
141 super(DirectoryHandler, self).__init__(
142 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400143
Brad Bishop87b63c12016-03-18 14:47:51 -0400144 def find(self, path='/'):
145 return self.try_mapper_call(
146 self.mapper.get_subtree_paths, path=path, depth=1)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400147
Brad Bishop87b63c12016-03-18 14:47:51 -0400148 def setup(self, path='/'):
149 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400150
Brad Bishop87b63c12016-03-18 14:47:51 -0400151 def do_get(self, path='/'):
152 return request.route_data['map']
153
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400154
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500155class ListNamesHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400156 verbs = 'GET'
157 rules = ['/list', '<path:path>/list']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400158
Brad Bishop87b63c12016-03-18 14:47:51 -0400159 def __init__(self, app, bus):
160 super(ListNamesHandler, self).__init__(
161 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400162
Brad Bishop87b63c12016-03-18 14:47:51 -0400163 def find(self, path='/'):
164 return self.try_mapper_call(
165 self.mapper.get_subtree, path=path).keys()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400166
Brad Bishop87b63c12016-03-18 14:47:51 -0400167 def setup(self, path='/'):
168 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400169
Brad Bishop87b63c12016-03-18 14:47:51 -0400170 def do_get(self, path='/'):
171 return request.route_data['map']
172
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400173
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500174class ListHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400175 verbs = 'GET'
176 rules = ['/enumerate', '<path:path>/enumerate']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400177
Brad Bishop87b63c12016-03-18 14:47:51 -0400178 def __init__(self, app, bus):
179 super(ListHandler, self).__init__(
180 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400181
Brad Bishop87b63c12016-03-18 14:47:51 -0400182 def find(self, path='/'):
183 return self.try_mapper_call(
184 self.mapper.get_subtree, path=path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400185
Brad Bishop87b63c12016-03-18 14:47:51 -0400186 def setup(self, path='/'):
187 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400188
Brad Bishop87b63c12016-03-18 14:47:51 -0400189 def do_get(self, path='/'):
Brad Bishop71527b42016-04-01 14:51:14 -0400190 return {x: y for x, y in self.mapper.enumerate_subtree(
191 path,
192 mapper_data=request.route_data['map']).dataitems()}
Brad Bishop87b63c12016-03-18 14:47:51 -0400193
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400194
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500195class MethodHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400196 verbs = 'POST'
197 rules = '<path:path>/action/<method>'
198 request_type = list
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400199
Brad Bishop87b63c12016-03-18 14:47:51 -0400200 def __init__(self, app, bus):
201 super(MethodHandler, self).__init__(
202 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400203
Brad Bishop87b63c12016-03-18 14:47:51 -0400204 def find(self, path, method):
205 busses = self.try_mapper_call(
206 self.mapper.get_object, path=path)
207 for items in busses.iteritems():
208 m = self.find_method_on_bus(path, method, *items)
209 if m:
210 return m
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400211
Brad Bishop87b63c12016-03-18 14:47:51 -0400212 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400213
Brad Bishop87b63c12016-03-18 14:47:51 -0400214 def setup(self, path, method):
215 request.route_data['method'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400216
Brad Bishop87b63c12016-03-18 14:47:51 -0400217 def do_post(self, path, method):
218 try:
219 if request.parameter_list:
220 return request.route_data['method'](*request.parameter_list)
221 else:
222 return request.route_data['method']()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400223
Brad Bishop87b63c12016-03-18 14:47:51 -0400224 except dbus.exceptions.DBusException, e:
225 if e.get_dbus_name() == DBUS_INVALID_ARGS:
226 abort(400, str(e))
227 if e.get_dbus_name() == DBUS_TYPE_ERROR:
228 abort(400, str(e))
229 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400230
Brad Bishop87b63c12016-03-18 14:47:51 -0400231 @staticmethod
232 def find_method_in_interface(method, obj, interface, methods):
233 if methods is None:
234 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400235
Brad Bishop6d190602016-04-15 13:09:39 -0400236 method = obmc.utils.misc.find_case_insensitive(method, methods.keys())
Brad Bishop87b63c12016-03-18 14:47:51 -0400237 if method is not None:
238 iface = dbus.Interface(obj, interface)
239 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400240
Brad Bishop87b63c12016-03-18 14:47:51 -0400241 def find_method_on_bus(self, path, method, bus, interfaces):
242 obj = self.bus.get_object(bus, path, introspect=False)
243 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
244 data = iface.Introspect()
245 parser = IntrospectionNodeParser(
246 ElementTree.fromstring(data),
Brad Bishopb103d2d2016-03-04 16:19:14 -0500247 intf_match=obmc.utils.misc.ListMatch(interfaces))
Brad Bishop87b63c12016-03-18 14:47:51 -0400248 for x, y in parser.get_interfaces().iteritems():
249 m = self.find_method_in_interface(
250 method, obj, x, y.get('method'))
251 if m:
252 return m
253
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400254
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500255class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400256 verbs = ['PUT', 'GET']
257 rules = '<path:path>/attr/<prop>'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400258
Brad Bishop87b63c12016-03-18 14:47:51 -0400259 def __init__(self, app, bus):
260 super(PropertyHandler, self).__init__(
261 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400262
Brad Bishop87b63c12016-03-18 14:47:51 -0400263 def find(self, path, prop):
264 self.app.instance_handler.setup(path)
265 obj = self.app.instance_handler.do_get(path)
266 try:
267 obj[prop]
268 except KeyError, e:
269 if request.method == 'PUT':
270 abort(403, _4034_msg % ('property', 'created', str(e)))
271 else:
272 abort(404, _4034_msg % ('property', 'found', str(e)))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400273
Brad Bishop87b63c12016-03-18 14:47:51 -0400274 return {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500275
Brad Bishop87b63c12016-03-18 14:47:51 -0400276 def setup(self, path, prop):
277 request.route_data['obj'] = self.find(path, prop)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500278
Brad Bishop87b63c12016-03-18 14:47:51 -0400279 def do_get(self, path, prop):
280 return request.route_data['obj'][path][prop]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500281
Brad Bishop87b63c12016-03-18 14:47:51 -0400282 def do_put(self, path, prop, value=None):
283 if value is None:
284 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500285
Brad Bishop87b63c12016-03-18 14:47:51 -0400286 prop, iface, properties_iface = self.get_host_interface(
287 path, prop, request.route_data['map'][path])
288 try:
289 properties_iface.Set(iface, prop, value)
290 except ValueError, e:
291 abort(400, str(e))
292 except dbus.exceptions.DBusException, e:
293 if e.get_dbus_name() == DBUS_INVALID_ARGS:
294 abort(403, str(e))
295 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500296
Brad Bishop87b63c12016-03-18 14:47:51 -0400297 def get_host_interface(self, path, prop, bus_info):
298 for bus, interfaces in bus_info.iteritems():
299 obj = self.bus.get_object(bus, path, introspect=True)
300 properties_iface = dbus.Interface(
301 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500302
Brad Bishop87b63c12016-03-18 14:47:51 -0400303 info = self.get_host_interface_on_bus(
304 path, prop, properties_iface, bus, interfaces)
305 if info is not None:
306 prop, iface = info
307 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500308
Brad Bishop87b63c12016-03-18 14:47:51 -0400309 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
310 for i in interfaces:
311 properties = self.try_properties_interface(iface.GetAll, i)
312 if properties is None:
313 continue
Brad Bishop8b0d3fa2016-11-28 15:41:47 -0500314 prop = obmc.utils.misc.find_case_insensitive(
315 prop, properties.keys())
Brad Bishop87b63c12016-03-18 14:47:51 -0400316 if prop is None:
317 continue
318 return prop, i
319
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500320
Brad Bishop2503bd62015-12-16 17:56:12 -0500321class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400322 verbs = ['GET']
323 rules = '<path:path>/schema'
Brad Bishop2503bd62015-12-16 17:56:12 -0500324
Brad Bishop87b63c12016-03-18 14:47:51 -0400325 def __init__(self, app, bus):
326 super(SchemaHandler, self).__init__(
327 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500328
Brad Bishop87b63c12016-03-18 14:47:51 -0400329 def find(self, path):
330 return self.try_mapper_call(
331 self.mapper.get_object,
332 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500333
Brad Bishop87b63c12016-03-18 14:47:51 -0400334 def setup(self, path):
335 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500336
Brad Bishop87b63c12016-03-18 14:47:51 -0400337 def do_get(self, path):
338 schema = {}
339 for x in request.route_data['map'].iterkeys():
340 obj = self.bus.get_object(x, path, introspect=False)
341 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
342 data = iface.Introspect()
343 parser = IntrospectionNodeParser(
344 ElementTree.fromstring(data))
345 for x, y in parser.get_interfaces().iteritems():
346 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500347
Brad Bishop87b63c12016-03-18 14:47:51 -0400348 return schema
349
Brad Bishop2503bd62015-12-16 17:56:12 -0500350
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500351class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400352 verbs = ['GET', 'PUT', 'DELETE']
353 rules = '<path:path>'
354 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500355
Brad Bishop87b63c12016-03-18 14:47:51 -0400356 def __init__(self, app, bus):
357 super(InstanceHandler, self).__init__(
358 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500359
Brad Bishop87b63c12016-03-18 14:47:51 -0400360 def find(self, path, callback=None):
361 return {path: self.try_mapper_call(
362 self.mapper.get_object,
363 callback,
364 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500365
Brad Bishop87b63c12016-03-18 14:47:51 -0400366 def setup(self, path):
367 callback = None
368 if request.method == 'PUT':
369 def callback(e, **kw):
370 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500371
Brad Bishop87b63c12016-03-18 14:47:51 -0400372 if request.route_data.get('map') is None:
373 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500374
Brad Bishop87b63c12016-03-18 14:47:51 -0400375 def do_get(self, path):
Brad Bishop71527b42016-04-01 14:51:14 -0400376 return self.mapper.enumerate_object(
377 path,
378 mapper_data=request.route_data['map'])
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500379
Brad Bishop87b63c12016-03-18 14:47:51 -0400380 def do_put(self, path):
381 # make sure all properties exist in the request
382 obj = set(self.do_get(path).keys())
383 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500384
Brad Bishop87b63c12016-03-18 14:47:51 -0400385 diff = list(obj.difference(req))
386 if diff:
387 abort(403, _4034_msg % (
388 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500389
Brad Bishop87b63c12016-03-18 14:47:51 -0400390 diff = list(req.difference(obj))
391 if diff:
392 abort(403, _4034_msg % (
393 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500394
Brad Bishop87b63c12016-03-18 14:47:51 -0400395 for p, v in request.parameter_list.iteritems():
396 self.app.property_handler.do_put(
397 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500398
Brad Bishop87b63c12016-03-18 14:47:51 -0400399 def do_delete(self, path):
400 for bus_info in request.route_data['map'][path].iteritems():
401 if self.bus_missing_delete(path, *bus_info):
402 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500403
Brad Bishop87b63c12016-03-18 14:47:51 -0400404 for bus in request.route_data['map'][path].iterkeys():
405 self.delete_on_bus(path, bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500406
Brad Bishop87b63c12016-03-18 14:47:51 -0400407 def bus_missing_delete(self, path, bus, interfaces):
408 return DELETE_IFACE not in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500409
Brad Bishop87b63c12016-03-18 14:47:51 -0400410 def delete_on_bus(self, path, bus):
411 obj = self.bus.get_object(bus, path, introspect=False)
412 delete_iface = dbus.Interface(
413 obj, dbus_interface=DELETE_IFACE)
414 delete_iface.Delete()
415
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500416
Brad Bishop2f428582015-12-02 10:56:11 -0500417class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400418 ''' Handles the /login and /logout routes, manages
419 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500420
Brad Bishop87b63c12016-03-18 14:47:51 -0400421 rules = ['/login', '/logout']
422 login_str = "User '%s' logged %s"
423 bad_passwd_str = "Invalid username or password"
424 no_user_str = "No user logged in"
425 bad_json_str = "Expecting request format { 'data': " \
426 "[<username>, <password>] }, got '%s'"
427 _require_auth = None
428 MAX_SESSIONS = 16
Brad Bishop2f428582015-12-02 10:56:11 -0500429
Brad Bishop87b63c12016-03-18 14:47:51 -0400430 def __init__(self, app, bus):
431 super(SessionHandler, self).__init__(
432 app, bus)
433 self.hmac_key = os.urandom(128)
434 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500435
Brad Bishop87b63c12016-03-18 14:47:51 -0400436 @staticmethod
437 def authenticate(username, clear):
438 try:
439 encoded = spwd.getspnam(username)[1]
440 return encoded == crypt.crypt(clear, encoded)
441 except KeyError:
442 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500443
Brad Bishop87b63c12016-03-18 14:47:51 -0400444 def invalidate_session(self, session):
445 try:
446 self.session_store.remove(session)
447 except ValueError:
448 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500449
Brad Bishop87b63c12016-03-18 14:47:51 -0400450 def new_session(self):
451 sid = os.urandom(32)
452 if self.MAX_SESSIONS <= len(self.session_store):
453 self.session_store.pop()
454 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500455
Brad Bishop87b63c12016-03-18 14:47:51 -0400456 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500457
Brad Bishop87b63c12016-03-18 14:47:51 -0400458 def get_session(self, sid):
459 sids = [x['sid'] for x in self.session_store]
460 try:
461 return self.session_store[sids.index(sid)]
462 except ValueError:
463 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500464
Brad Bishop87b63c12016-03-18 14:47:51 -0400465 def get_session_from_cookie(self):
466 return self.get_session(
467 request.get_cookie(
468 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500469
Brad Bishop87b63c12016-03-18 14:47:51 -0400470 def do_post(self, **kw):
471 if request.path == '/login':
472 return self.do_login(**kw)
473 else:
474 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500475
Brad Bishop87b63c12016-03-18 14:47:51 -0400476 def do_logout(self, **kw):
477 session = self.get_session_from_cookie()
478 if session is not None:
479 user = session['user']
480 self.invalidate_session(session)
481 response.delete_cookie('sid')
482 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500483
Brad Bishop87b63c12016-03-18 14:47:51 -0400484 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500485
Brad Bishop87b63c12016-03-18 14:47:51 -0400486 def do_login(self, **kw):
487 session = self.get_session_from_cookie()
488 if session is not None:
489 return self.login_str % (session['user'], 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500490
Brad Bishop87b63c12016-03-18 14:47:51 -0400491 if len(request.parameter_list) != 2:
492 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500493
Brad Bishop87b63c12016-03-18 14:47:51 -0400494 if not self.authenticate(*request.parameter_list):
Brad Bishopdc3fbfa2016-09-08 09:51:38 -0400495 abort(401, self.bad_passwd_str)
Brad Bishop2f428582015-12-02 10:56:11 -0500496
Brad Bishop87b63c12016-03-18 14:47:51 -0400497 user = request.parameter_list[0]
498 session = self.new_session()
499 session['user'] = user
500 response.set_cookie(
501 'sid', session['sid'], secret=self.hmac_key,
502 secure=True,
503 httponly=True)
504 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500505
Brad Bishop87b63c12016-03-18 14:47:51 -0400506 def find(self, **kw):
507 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500508
Brad Bishop87b63c12016-03-18 14:47:51 -0400509 def setup(self, **kw):
510 pass
511
Brad Bishop2f428582015-12-02 10:56:11 -0500512
513class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400514 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500515
Brad Bishop87b63c12016-03-18 14:47:51 -0400516 name = 'authorization'
517 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -0500518
Brad Bishop87b63c12016-03-18 14:47:51 -0400519 class Compose:
520 def __init__(self, validators, callback, session_mgr):
521 self.validators = validators
522 self.callback = callback
523 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -0500524
Brad Bishop87b63c12016-03-18 14:47:51 -0400525 def __call__(self, *a, **kw):
526 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
527 session = self.session_mgr.get_session(sid)
528 for x in self.validators:
529 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500530
Brad Bishop87b63c12016-03-18 14:47:51 -0400531 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500532
Brad Bishop87b63c12016-03-18 14:47:51 -0400533 def apply(self, callback, route):
534 undecorated = route.get_undecorated_callback()
535 if not isinstance(undecorated, RouteHandler):
536 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500537
Brad Bishop87b63c12016-03-18 14:47:51 -0400538 auth_types = getattr(
539 undecorated, '_require_auth', None)
540 if not auth_types:
541 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500542
Brad Bishop87b63c12016-03-18 14:47:51 -0400543 return self.Compose(
544 auth_types, callback, undecorated.app.session_handler)
545
Brad Bishop2f428582015-12-02 10:56:11 -0500546
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500547class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400548 ''' Ensures request content satisfies the OpenBMC json api format. '''
549 name = 'json_api_request'
550 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500551
Brad Bishop87b63c12016-03-18 14:47:51 -0400552 error_str = "Expecting request format { 'data': <value> }, got '%s'"
553 type_error_str = "Unsupported Content-Type: '%s'"
554 json_type = "application/json"
555 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500556
Brad Bishop87b63c12016-03-18 14:47:51 -0400557 @staticmethod
558 def content_expected():
559 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500560
Brad Bishop87b63c12016-03-18 14:47:51 -0400561 def validate_request(self):
562 if request.content_length > 0 and \
563 request.content_type != self.json_type:
564 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500565
Brad Bishop87b63c12016-03-18 14:47:51 -0400566 try:
567 request.parameter_list = request.json.get('data')
568 except ValueError, e:
569 abort(400, str(e))
570 except (AttributeError, KeyError, TypeError):
571 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500572
Brad Bishop87b63c12016-03-18 14:47:51 -0400573 def apply(self, callback, route):
574 verbs = getattr(
575 route.get_undecorated_callback(), '_verbs', None)
576 if verbs is None:
577 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500578
Brad Bishop87b63c12016-03-18 14:47:51 -0400579 if not set(self.request_methods).intersection(verbs):
580 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500581
Brad Bishop87b63c12016-03-18 14:47:51 -0400582 def wrap(*a, **kw):
583 if self.content_expected():
584 self.validate_request()
585 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500586
Brad Bishop87b63c12016-03-18 14:47:51 -0400587 return wrap
588
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500589
590class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400591 ''' Ensures request content type satisfies the OpenBMC json api format. '''
592 name = 'json_api_method_request'
593 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500594
Brad Bishop87b63c12016-03-18 14:47:51 -0400595 error_str = "Expecting request format { 'data': %s }, got '%s'"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500596
Brad Bishop87b63c12016-03-18 14:47:51 -0400597 def apply(self, callback, route):
598 request_type = getattr(
599 route.get_undecorated_callback(), 'request_type', None)
600 if request_type is None:
601 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500602
Brad Bishop87b63c12016-03-18 14:47:51 -0400603 def validate_request():
604 if not isinstance(request.parameter_list, request_type):
605 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500606
Brad Bishop87b63c12016-03-18 14:47:51 -0400607 def wrap(*a, **kw):
608 if JsonApiRequestPlugin.content_expected():
609 validate_request()
610 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500611
Brad Bishop87b63c12016-03-18 14:47:51 -0400612 return wrap
613
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500614
Brad Bishop080a48e2017-02-21 22:34:43 -0500615class JsonErrorsPlugin(JSONPlugin):
616 ''' Extend the Bottle JSONPlugin such that it also encodes error
617 responses. '''
618
619 def __init__(self, app, **kw):
620 super(JsonErrorsPlugin, self).__init__(**kw)
621 self.json_opts = {
622 x: y for x, y in kw.iteritems()
623 if x in ['indent', 'sort_keys']}
624 app.install_error_callback(self.error_callback)
625
626 def error_callback(self, response_object, response_body, **kw):
627 response_body['body'] = json.dumps(response_object, **self.json_opts)
628 response.content_type = 'application/json'
629
630
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500631class JsonApiResponsePlugin(object):
Brad Bishop080a48e2017-02-21 22:34:43 -0500632 ''' Emits responses in the OpenBMC json api format. '''
Brad Bishop87b63c12016-03-18 14:47:51 -0400633 name = 'json_api_response'
634 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500635
Brad Bishop080a48e2017-02-21 22:34:43 -0500636 def __init__(self, app):
637 app.install_error_callback(self.error_callback)
638
Brad Bishop87b63c12016-03-18 14:47:51 -0400639 def apply(self, callback, route):
640 def wrap(*a, **kw):
641 resp = {'data': callback(*a, **kw)}
642 resp['status'] = 'ok'
643 resp['message'] = response.status_line
644 return resp
645 return wrap
646
Brad Bishop080a48e2017-02-21 22:34:43 -0500647 def error_callback(self, error, response_object, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -0400648 response_object['message'] = error.status_line
Brad Bishop080a48e2017-02-21 22:34:43 -0500649 response_object.setdefault('data', {})['description'] = str(error.body)
Brad Bishop87b63c12016-03-18 14:47:51 -0400650 if error.status_code == 500:
651 response_object['data']['exception'] = repr(error.exception)
652 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500653
Brad Bishop87b63c12016-03-18 14:47:51 -0400654
Brad Bishop080a48e2017-02-21 22:34:43 -0500655class JsonpPlugin(object):
Brad Bishop80fe37a2016-03-29 10:54:54 -0400656 ''' Json javascript wrapper. '''
657 name = 'jsonp'
658 api = 2
659
Brad Bishop080a48e2017-02-21 22:34:43 -0500660 def __init__(self, app, **kw):
661 app.install_error_callback(self.error_callback)
Brad Bishop80fe37a2016-03-29 10:54:54 -0400662
663 @staticmethod
664 def to_jsonp(json):
665 jwrapper = request.query.callback or None
666 if(jwrapper):
667 response.set_header('Content-Type', 'application/javascript')
668 json = jwrapper + '(' + json + ');'
669 return json
670
671 def apply(self, callback, route):
672 def wrap(*a, **kw):
673 return self.to_jsonp(callback(*a, **kw))
674 return wrap
675
Brad Bishop080a48e2017-02-21 22:34:43 -0500676 def error_callback(self, response_body, **kw):
677 response_body['body'] = self.to_jsonp(response_body['body'])
Brad Bishop80fe37a2016-03-29 10:54:54 -0400678
679
Brad Bishop2c6fc762016-08-29 15:53:25 -0400680class App(Bottle):
Brad Bishop2ddfa002016-08-29 15:11:55 -0400681 def __init__(self):
Brad Bishop2c6fc762016-08-29 15:53:25 -0400682 super(App, self).__init__(autojson=False)
Brad Bishop2ddfa002016-08-29 15:11:55 -0400683 self.bus = dbus.SystemBus()
684 self.mapper = obmc.mapper.Mapper(self.bus)
Brad Bishop080a48e2017-02-21 22:34:43 -0500685 self.error_callbacks = []
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500686
Brad Bishop87b63c12016-03-18 14:47:51 -0400687 self.install_hooks()
688 self.install_plugins()
689 self.create_handlers()
690 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500691
Brad Bishop87b63c12016-03-18 14:47:51 -0400692 def install_plugins(self):
693 # install json api plugins
694 json_kw = {'indent': 2, 'sort_keys': True}
Brad Bishop87b63c12016-03-18 14:47:51 -0400695 self.install(AuthorizationPlugin())
Brad Bishop080a48e2017-02-21 22:34:43 -0500696 self.install(JsonpPlugin(self, **json_kw))
697 self.install(JsonErrorsPlugin(self, **json_kw))
698 self.install(JsonApiResponsePlugin(self))
Brad Bishop87b63c12016-03-18 14:47:51 -0400699 self.install(JsonApiRequestPlugin())
700 self.install(JsonApiRequestTypePlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500701
Brad Bishop87b63c12016-03-18 14:47:51 -0400702 def install_hooks(self):
Brad Bishop080a48e2017-02-21 22:34:43 -0500703 self.error_handler_type = type(self.default_error_handler)
704 self.original_error_handler = self.default_error_handler
705 self.default_error_handler = self.error_handler_type(
706 self.custom_error_handler, self, Bottle)
707
Brad Bishop87b63c12016-03-18 14:47:51 -0400708 self.real_router_match = self.router.match
709 self.router.match = self.custom_router_match
710 self.add_hook('before_request', self.strip_extra_slashes)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500711
Brad Bishop87b63c12016-03-18 14:47:51 -0400712 def create_handlers(self):
713 # create route handlers
714 self.session_handler = SessionHandler(self, self.bus)
715 self.directory_handler = DirectoryHandler(self, self.bus)
716 self.list_names_handler = ListNamesHandler(self, self.bus)
717 self.list_handler = ListHandler(self, self.bus)
718 self.method_handler = MethodHandler(self, self.bus)
719 self.property_handler = PropertyHandler(self, self.bus)
720 self.schema_handler = SchemaHandler(self, self.bus)
721 self.instance_handler = InstanceHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500722
Brad Bishop87b63c12016-03-18 14:47:51 -0400723 def install_handlers(self):
724 self.session_handler.install()
725 self.directory_handler.install()
726 self.list_names_handler.install()
727 self.list_handler.install()
728 self.method_handler.install()
729 self.property_handler.install()
730 self.schema_handler.install()
731 # this has to come last, since it matches everything
732 self.instance_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500733
Brad Bishop080a48e2017-02-21 22:34:43 -0500734 def install_error_callback(self, callback):
735 self.error_callbacks.insert(0, callback)
736
Brad Bishop87b63c12016-03-18 14:47:51 -0400737 def custom_router_match(self, environ):
738 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
739 needed doesn't work for us since the instance rules match
740 everything. This monkey-patch lets the route handler figure
741 out which response is needed. This could be accomplished
742 with a hook but that would require calling the router match
743 function twice.
744 '''
745 route, args = self.real_router_match(environ)
746 if isinstance(route.callback, RouteHandler):
747 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500748
Brad Bishop87b63c12016-03-18 14:47:51 -0400749 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500750
Brad Bishop080a48e2017-02-21 22:34:43 -0500751 def custom_error_handler(self, res, error):
752 ''' Allow plugins to modify error reponses too via this custom
753 error handler. '''
754
755 response_object = {}
756 response_body = {}
757 for x in self.error_callbacks:
758 x(error=error,
759 response_object=response_object,
760 response_body=response_body)
761
762 return response_body.get('body', "")
763
Brad Bishop87b63c12016-03-18 14:47:51 -0400764 @staticmethod
765 def strip_extra_slashes():
766 path = request.environ['PATH_INFO']
767 trailing = ("", "/")[path[-1] == '/']
768 parts = filter(bool, path.split('/'))
769 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing