blob: 93f0895c3f8886d86a4dc26704c4d65fd2594fb9 [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 Bishop87b63c12016-03-18 14:47:51 -040077 def _setup(self, **kw):
78 request.route_data = {}
79 if request.method in self._verbs:
80 return self.setup(**kw)
81 else:
82 self.find(**kw)
83 raise HTTPError(
84 405, "Method not allowed.", Allow=','.join(self._verbs))
Brad Bishopaa65f6e2015-10-27 16:28:51 -040085
Brad Bishop87b63c12016-03-18 14:47:51 -040086 def __call__(self, **kw):
87 return getattr(self, 'do_' + request.method.lower())(**kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -040088
Brad Bishop87b63c12016-03-18 14:47:51 -040089 def install(self):
90 self.app.route(
91 self._rules, callback=self,
92 method=['GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
Brad Bishopaa65f6e2015-10-27 16:28:51 -040093
Brad Bishop87b63c12016-03-18 14:47:51 -040094 @staticmethod
95 def try_mapper_call(f, callback=None, **kw):
96 try:
97 return f(**kw)
98 except dbus.exceptions.DBusException, e:
Brad Bishopfce77562016-11-28 15:44:18 -050099 if e.get_dbus_name() == \
100 'org.freedesktop.DBus.Error.ObjectPathInUse':
101 abort(503, str(e))
Brad Bishopb103d2d2016-03-04 16:19:14 -0500102 if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
Brad Bishop87b63c12016-03-18 14:47:51 -0400103 raise
104 if callback is None:
105 def callback(e, **kw):
106 abort(404, str(e))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400107
Brad Bishop87b63c12016-03-18 14:47:51 -0400108 callback(e, **kw)
109
110 @staticmethod
111 def try_properties_interface(f, *a):
112 try:
113 return f(*a)
114 except dbus.exceptions.DBusException, e:
115 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_message():
116 # interface doesn't have any properties
117 return None
Brad Bishopf4e74982016-04-01 14:53:05 -0400118 if DBUS_UNKNOWN_INTERFACE_ERROR in e.get_dbus_name():
119 # interface doesn't have any properties
120 return None
Brad Bishop87b63c12016-03-18 14:47:51 -0400121 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
122 # properties interface not implemented at all
123 return None
124 raise
125
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400126
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500127class DirectoryHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400128 verbs = 'GET'
129 rules = '<path:path>/'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400130
Brad Bishop87b63c12016-03-18 14:47:51 -0400131 def __init__(self, app, bus):
132 super(DirectoryHandler, self).__init__(
133 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400134
Brad Bishop87b63c12016-03-18 14:47:51 -0400135 def find(self, path='/'):
136 return self.try_mapper_call(
137 self.mapper.get_subtree_paths, path=path, depth=1)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400138
Brad Bishop87b63c12016-03-18 14:47:51 -0400139 def setup(self, path='/'):
140 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400141
Brad Bishop87b63c12016-03-18 14:47:51 -0400142 def do_get(self, path='/'):
143 return request.route_data['map']
144
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400145
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500146class ListNamesHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400147 verbs = 'GET'
148 rules = ['/list', '<path:path>/list']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400149
Brad Bishop87b63c12016-03-18 14:47:51 -0400150 def __init__(self, app, bus):
151 super(ListNamesHandler, self).__init__(
152 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400153
Brad Bishop87b63c12016-03-18 14:47:51 -0400154 def find(self, path='/'):
155 return self.try_mapper_call(
156 self.mapper.get_subtree, path=path).keys()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400157
Brad Bishop87b63c12016-03-18 14:47:51 -0400158 def setup(self, path='/'):
159 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400160
Brad Bishop87b63c12016-03-18 14:47:51 -0400161 def do_get(self, path='/'):
162 return request.route_data['map']
163
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400164
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500165class ListHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400166 verbs = 'GET'
167 rules = ['/enumerate', '<path:path>/enumerate']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400168
Brad Bishop87b63c12016-03-18 14:47:51 -0400169 def __init__(self, app, bus):
170 super(ListHandler, self).__init__(
171 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400172
Brad Bishop87b63c12016-03-18 14:47:51 -0400173 def find(self, path='/'):
174 return self.try_mapper_call(
175 self.mapper.get_subtree, path=path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400176
Brad Bishop87b63c12016-03-18 14:47:51 -0400177 def setup(self, path='/'):
178 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400179
Brad Bishop87b63c12016-03-18 14:47:51 -0400180 def do_get(self, path='/'):
Brad Bishop71527b42016-04-01 14:51:14 -0400181 return {x: y for x, y in self.mapper.enumerate_subtree(
182 path,
183 mapper_data=request.route_data['map']).dataitems()}
Brad Bishop87b63c12016-03-18 14:47:51 -0400184
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400185
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500186class MethodHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400187 verbs = 'POST'
188 rules = '<path:path>/action/<method>'
189 request_type = list
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400190
Brad Bishop87b63c12016-03-18 14:47:51 -0400191 def __init__(self, app, bus):
192 super(MethodHandler, self).__init__(
193 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400194
Brad Bishop87b63c12016-03-18 14:47:51 -0400195 def find(self, path, method):
196 busses = self.try_mapper_call(
197 self.mapper.get_object, path=path)
198 for items in busses.iteritems():
199 m = self.find_method_on_bus(path, method, *items)
200 if m:
201 return m
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400202
Brad Bishop87b63c12016-03-18 14:47:51 -0400203 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400204
Brad Bishop87b63c12016-03-18 14:47:51 -0400205 def setup(self, path, method):
206 request.route_data['method'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400207
Brad Bishop87b63c12016-03-18 14:47:51 -0400208 def do_post(self, path, method):
209 try:
210 if request.parameter_list:
211 return request.route_data['method'](*request.parameter_list)
212 else:
213 return request.route_data['method']()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400214
Brad Bishop87b63c12016-03-18 14:47:51 -0400215 except dbus.exceptions.DBusException, e:
216 if e.get_dbus_name() == DBUS_INVALID_ARGS:
217 abort(400, str(e))
218 if e.get_dbus_name() == DBUS_TYPE_ERROR:
219 abort(400, str(e))
220 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400221
Brad Bishop87b63c12016-03-18 14:47:51 -0400222 @staticmethod
223 def find_method_in_interface(method, obj, interface, methods):
224 if methods is None:
225 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400226
Brad Bishop6d190602016-04-15 13:09:39 -0400227 method = obmc.utils.misc.find_case_insensitive(method, methods.keys())
Brad Bishop87b63c12016-03-18 14:47:51 -0400228 if method is not None:
229 iface = dbus.Interface(obj, interface)
230 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400231
Brad Bishop87b63c12016-03-18 14:47:51 -0400232 def find_method_on_bus(self, path, method, bus, interfaces):
233 obj = self.bus.get_object(bus, path, introspect=False)
234 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
235 data = iface.Introspect()
236 parser = IntrospectionNodeParser(
237 ElementTree.fromstring(data),
Brad Bishopb103d2d2016-03-04 16:19:14 -0500238 intf_match=obmc.utils.misc.ListMatch(interfaces))
Brad Bishop87b63c12016-03-18 14:47:51 -0400239 for x, y in parser.get_interfaces().iteritems():
240 m = self.find_method_in_interface(
241 method, obj, x, y.get('method'))
242 if m:
243 return m
244
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400245
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500246class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400247 verbs = ['PUT', 'GET']
248 rules = '<path:path>/attr/<prop>'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400249
Brad Bishop87b63c12016-03-18 14:47:51 -0400250 def __init__(self, app, bus):
251 super(PropertyHandler, self).__init__(
252 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400253
Brad Bishop87b63c12016-03-18 14:47:51 -0400254 def find(self, path, prop):
255 self.app.instance_handler.setup(path)
256 obj = self.app.instance_handler.do_get(path)
257 try:
258 obj[prop]
259 except KeyError, e:
260 if request.method == 'PUT':
261 abort(403, _4034_msg % ('property', 'created', str(e)))
262 else:
263 abort(404, _4034_msg % ('property', 'found', str(e)))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400264
Brad Bishop87b63c12016-03-18 14:47:51 -0400265 return {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500266
Brad Bishop87b63c12016-03-18 14:47:51 -0400267 def setup(self, path, prop):
268 request.route_data['obj'] = self.find(path, prop)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500269
Brad Bishop87b63c12016-03-18 14:47:51 -0400270 def do_get(self, path, prop):
271 return request.route_data['obj'][path][prop]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500272
Brad Bishop87b63c12016-03-18 14:47:51 -0400273 def do_put(self, path, prop, value=None):
274 if value is None:
275 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500276
Brad Bishop87b63c12016-03-18 14:47:51 -0400277 prop, iface, properties_iface = self.get_host_interface(
278 path, prop, request.route_data['map'][path])
279 try:
280 properties_iface.Set(iface, prop, value)
281 except ValueError, e:
282 abort(400, str(e))
283 except dbus.exceptions.DBusException, e:
284 if e.get_dbus_name() == DBUS_INVALID_ARGS:
285 abort(403, str(e))
286 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500287
Brad Bishop87b63c12016-03-18 14:47:51 -0400288 def get_host_interface(self, path, prop, bus_info):
289 for bus, interfaces in bus_info.iteritems():
290 obj = self.bus.get_object(bus, path, introspect=True)
291 properties_iface = dbus.Interface(
292 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500293
Brad Bishop87b63c12016-03-18 14:47:51 -0400294 info = self.get_host_interface_on_bus(
295 path, prop, properties_iface, bus, interfaces)
296 if info is not None:
297 prop, iface = info
298 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500299
Brad Bishop87b63c12016-03-18 14:47:51 -0400300 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
301 for i in interfaces:
302 properties = self.try_properties_interface(iface.GetAll, i)
303 if properties is None:
304 continue
Brad Bishop8b0d3fa2016-11-28 15:41:47 -0500305 prop = obmc.utils.misc.find_case_insensitive(
306 prop, properties.keys())
Brad Bishop87b63c12016-03-18 14:47:51 -0400307 if prop is None:
308 continue
309 return prop, i
310
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500311
Brad Bishop2503bd62015-12-16 17:56:12 -0500312class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400313 verbs = ['GET']
314 rules = '<path:path>/schema'
Brad Bishop2503bd62015-12-16 17:56:12 -0500315
Brad Bishop87b63c12016-03-18 14:47:51 -0400316 def __init__(self, app, bus):
317 super(SchemaHandler, self).__init__(
318 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500319
Brad Bishop87b63c12016-03-18 14:47:51 -0400320 def find(self, path):
321 return self.try_mapper_call(
322 self.mapper.get_object,
323 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500324
Brad Bishop87b63c12016-03-18 14:47:51 -0400325 def setup(self, path):
326 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500327
Brad Bishop87b63c12016-03-18 14:47:51 -0400328 def do_get(self, path):
329 schema = {}
330 for x in request.route_data['map'].iterkeys():
331 obj = self.bus.get_object(x, path, introspect=False)
332 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
333 data = iface.Introspect()
334 parser = IntrospectionNodeParser(
335 ElementTree.fromstring(data))
336 for x, y in parser.get_interfaces().iteritems():
337 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500338
Brad Bishop87b63c12016-03-18 14:47:51 -0400339 return schema
340
Brad Bishop2503bd62015-12-16 17:56:12 -0500341
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500342class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400343 verbs = ['GET', 'PUT', 'DELETE']
344 rules = '<path:path>'
345 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500346
Brad Bishop87b63c12016-03-18 14:47:51 -0400347 def __init__(self, app, bus):
348 super(InstanceHandler, self).__init__(
349 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500350
Brad Bishop87b63c12016-03-18 14:47:51 -0400351 def find(self, path, callback=None):
352 return {path: self.try_mapper_call(
353 self.mapper.get_object,
354 callback,
355 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500356
Brad Bishop87b63c12016-03-18 14:47:51 -0400357 def setup(self, path):
358 callback = None
359 if request.method == 'PUT':
360 def callback(e, **kw):
361 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500362
Brad Bishop87b63c12016-03-18 14:47:51 -0400363 if request.route_data.get('map') is None:
364 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500365
Brad Bishop87b63c12016-03-18 14:47:51 -0400366 def do_get(self, path):
Brad Bishop71527b42016-04-01 14:51:14 -0400367 return self.mapper.enumerate_object(
368 path,
369 mapper_data=request.route_data['map'])
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500370
Brad Bishop87b63c12016-03-18 14:47:51 -0400371 def do_put(self, path):
372 # make sure all properties exist in the request
373 obj = set(self.do_get(path).keys())
374 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500375
Brad Bishop87b63c12016-03-18 14:47:51 -0400376 diff = list(obj.difference(req))
377 if diff:
378 abort(403, _4034_msg % (
379 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500380
Brad Bishop87b63c12016-03-18 14:47:51 -0400381 diff = list(req.difference(obj))
382 if diff:
383 abort(403, _4034_msg % (
384 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500385
Brad Bishop87b63c12016-03-18 14:47:51 -0400386 for p, v in request.parameter_list.iteritems():
387 self.app.property_handler.do_put(
388 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500389
Brad Bishop87b63c12016-03-18 14:47:51 -0400390 def do_delete(self, path):
391 for bus_info in request.route_data['map'][path].iteritems():
392 if self.bus_missing_delete(path, *bus_info):
393 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500394
Brad Bishop87b63c12016-03-18 14:47:51 -0400395 for bus in request.route_data['map'][path].iterkeys():
396 self.delete_on_bus(path, bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500397
Brad Bishop87b63c12016-03-18 14:47:51 -0400398 def bus_missing_delete(self, path, bus, interfaces):
399 return DELETE_IFACE not in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500400
Brad Bishop87b63c12016-03-18 14:47:51 -0400401 def delete_on_bus(self, path, bus):
402 obj = self.bus.get_object(bus, path, introspect=False)
403 delete_iface = dbus.Interface(
404 obj, dbus_interface=DELETE_IFACE)
405 delete_iface.Delete()
406
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500407
Brad Bishop2f428582015-12-02 10:56:11 -0500408class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400409 ''' Handles the /login and /logout routes, manages
410 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500411
Brad Bishop87b63c12016-03-18 14:47:51 -0400412 rules = ['/login', '/logout']
413 login_str = "User '%s' logged %s"
414 bad_passwd_str = "Invalid username or password"
415 no_user_str = "No user logged in"
416 bad_json_str = "Expecting request format { 'data': " \
417 "[<username>, <password>] }, got '%s'"
418 _require_auth = None
419 MAX_SESSIONS = 16
Brad Bishop2f428582015-12-02 10:56:11 -0500420
Brad Bishop87b63c12016-03-18 14:47:51 -0400421 def __init__(self, app, bus):
422 super(SessionHandler, self).__init__(
423 app, bus)
424 self.hmac_key = os.urandom(128)
425 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500426
Brad Bishop87b63c12016-03-18 14:47:51 -0400427 @staticmethod
428 def authenticate(username, clear):
429 try:
430 encoded = spwd.getspnam(username)[1]
431 return encoded == crypt.crypt(clear, encoded)
432 except KeyError:
433 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500434
Brad Bishop87b63c12016-03-18 14:47:51 -0400435 def invalidate_session(self, session):
436 try:
437 self.session_store.remove(session)
438 except ValueError:
439 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500440
Brad Bishop87b63c12016-03-18 14:47:51 -0400441 def new_session(self):
442 sid = os.urandom(32)
443 if self.MAX_SESSIONS <= len(self.session_store):
444 self.session_store.pop()
445 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500446
Brad Bishop87b63c12016-03-18 14:47:51 -0400447 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500448
Brad Bishop87b63c12016-03-18 14:47:51 -0400449 def get_session(self, sid):
450 sids = [x['sid'] for x in self.session_store]
451 try:
452 return self.session_store[sids.index(sid)]
453 except ValueError:
454 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500455
Brad Bishop87b63c12016-03-18 14:47:51 -0400456 def get_session_from_cookie(self):
457 return self.get_session(
458 request.get_cookie(
459 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500460
Brad Bishop87b63c12016-03-18 14:47:51 -0400461 def do_post(self, **kw):
462 if request.path == '/login':
463 return self.do_login(**kw)
464 else:
465 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500466
Brad Bishop87b63c12016-03-18 14:47:51 -0400467 def do_logout(self, **kw):
468 session = self.get_session_from_cookie()
469 if session is not None:
470 user = session['user']
471 self.invalidate_session(session)
472 response.delete_cookie('sid')
473 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500474
Brad Bishop87b63c12016-03-18 14:47:51 -0400475 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500476
Brad Bishop87b63c12016-03-18 14:47:51 -0400477 def do_login(self, **kw):
478 session = self.get_session_from_cookie()
479 if session is not None:
480 return self.login_str % (session['user'], 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500481
Brad Bishop87b63c12016-03-18 14:47:51 -0400482 if len(request.parameter_list) != 2:
483 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500484
Brad Bishop87b63c12016-03-18 14:47:51 -0400485 if not self.authenticate(*request.parameter_list):
Brad Bishopdc3fbfa2016-09-08 09:51:38 -0400486 abort(401, self.bad_passwd_str)
Brad Bishop2f428582015-12-02 10:56:11 -0500487
Brad Bishop87b63c12016-03-18 14:47:51 -0400488 user = request.parameter_list[0]
489 session = self.new_session()
490 session['user'] = user
491 response.set_cookie(
492 'sid', session['sid'], secret=self.hmac_key,
493 secure=True,
494 httponly=True)
495 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500496
Brad Bishop87b63c12016-03-18 14:47:51 -0400497 def find(self, **kw):
498 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500499
Brad Bishop87b63c12016-03-18 14:47:51 -0400500 def setup(self, **kw):
501 pass
502
Brad Bishop2f428582015-12-02 10:56:11 -0500503
504class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400505 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500506
Brad Bishop87b63c12016-03-18 14:47:51 -0400507 name = 'authorization'
508 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -0500509
Brad Bishop87b63c12016-03-18 14:47:51 -0400510 class Compose:
511 def __init__(self, validators, callback, session_mgr):
512 self.validators = validators
513 self.callback = callback
514 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -0500515
Brad Bishop87b63c12016-03-18 14:47:51 -0400516 def __call__(self, *a, **kw):
517 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
518 session = self.session_mgr.get_session(sid)
519 for x in self.validators:
520 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500521
Brad Bishop87b63c12016-03-18 14:47:51 -0400522 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500523
Brad Bishop87b63c12016-03-18 14:47:51 -0400524 def apply(self, callback, route):
525 undecorated = route.get_undecorated_callback()
526 if not isinstance(undecorated, RouteHandler):
527 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500528
Brad Bishop87b63c12016-03-18 14:47:51 -0400529 auth_types = getattr(
530 undecorated, '_require_auth', None)
531 if not auth_types:
532 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500533
Brad Bishop87b63c12016-03-18 14:47:51 -0400534 return self.Compose(
535 auth_types, callback, undecorated.app.session_handler)
536
Brad Bishop2f428582015-12-02 10:56:11 -0500537
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500538class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400539 ''' Ensures request content satisfies the OpenBMC json api format. '''
540 name = 'json_api_request'
541 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500542
Brad Bishop87b63c12016-03-18 14:47:51 -0400543 error_str = "Expecting request format { 'data': <value> }, got '%s'"
544 type_error_str = "Unsupported Content-Type: '%s'"
545 json_type = "application/json"
546 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500547
Brad Bishop87b63c12016-03-18 14:47:51 -0400548 @staticmethod
549 def content_expected():
550 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500551
Brad Bishop87b63c12016-03-18 14:47:51 -0400552 def validate_request(self):
553 if request.content_length > 0 and \
554 request.content_type != self.json_type:
555 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500556
Brad Bishop87b63c12016-03-18 14:47:51 -0400557 try:
558 request.parameter_list = request.json.get('data')
559 except ValueError, e:
560 abort(400, str(e))
561 except (AttributeError, KeyError, TypeError):
562 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500563
Brad Bishop87b63c12016-03-18 14:47:51 -0400564 def apply(self, callback, route):
565 verbs = getattr(
566 route.get_undecorated_callback(), '_verbs', None)
567 if verbs is None:
568 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500569
Brad Bishop87b63c12016-03-18 14:47:51 -0400570 if not set(self.request_methods).intersection(verbs):
571 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500572
Brad Bishop87b63c12016-03-18 14:47:51 -0400573 def wrap(*a, **kw):
574 if self.content_expected():
575 self.validate_request()
576 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500577
Brad Bishop87b63c12016-03-18 14:47:51 -0400578 return wrap
579
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500580
581class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400582 ''' Ensures request content type satisfies the OpenBMC json api format. '''
583 name = 'json_api_method_request'
584 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500585
Brad Bishop87b63c12016-03-18 14:47:51 -0400586 error_str = "Expecting request format { 'data': %s }, got '%s'"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500587
Brad Bishop87b63c12016-03-18 14:47:51 -0400588 def apply(self, callback, route):
589 request_type = getattr(
590 route.get_undecorated_callback(), 'request_type', None)
591 if request_type is None:
592 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500593
Brad Bishop87b63c12016-03-18 14:47:51 -0400594 def validate_request():
595 if not isinstance(request.parameter_list, request_type):
596 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500597
Brad Bishop87b63c12016-03-18 14:47:51 -0400598 def wrap(*a, **kw):
599 if JsonApiRequestPlugin.content_expected():
600 validate_request()
601 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500602
Brad Bishop87b63c12016-03-18 14:47:51 -0400603 return wrap
604
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500605
606class JsonApiResponsePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400607 ''' Emits normal responses in the OpenBMC json api format. '''
608 name = 'json_api_response'
609 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500610
Brad Bishop87b63c12016-03-18 14:47:51 -0400611 def apply(self, callback, route):
612 def wrap(*a, **kw):
613 resp = {'data': callback(*a, **kw)}
614 resp['status'] = 'ok'
615 resp['message'] = response.status_line
616 return resp
617 return wrap
618
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500619
620class JsonApiErrorsPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400621 ''' Emits error responses in the OpenBMC json api format. '''
622 name = 'json_api_errors'
623 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500624
Brad Bishop87b63c12016-03-18 14:47:51 -0400625 def __init__(self, **kw):
626 self.app = None
627 self.function_type = None
628 self.original = None
629 self.json_opts = {
630 x: y for x, y in kw.iteritems()
631 if x in ['indent', 'sort_keys']}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500632
Brad Bishop87b63c12016-03-18 14:47:51 -0400633 def setup(self, app):
634 self.app = app
635 self.function_type = type(app.default_error_handler)
636 self.original = app.default_error_handler
637 self.app.default_error_handler = self.function_type(
638 self.json_errors, app, Bottle)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500639
Brad Bishop87b63c12016-03-18 14:47:51 -0400640 def apply(self, callback, route):
641 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500642
Brad Bishop87b63c12016-03-18 14:47:51 -0400643 def close(self):
644 self.app.default_error_handler = self.function_type(
645 self.original, self.app, Bottle)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500646
Brad Bishop87b63c12016-03-18 14:47:51 -0400647 def json_errors(self, res, error):
648 response_object = {'status': 'error', 'data': {}}
649 response_object['message'] = error.status_line
650 response_object['data']['description'] = str(error.body)
651 if error.status_code == 500:
652 response_object['data']['exception'] = repr(error.exception)
653 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500654
Brad Bishop87b63c12016-03-18 14:47:51 -0400655 json_response = json.dumps(response_object, **self.json_opts)
656 response.content_type = 'application/json'
657 return json_response
658
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500659
Brad Bishop80fe37a2016-03-29 10:54:54 -0400660class JsonpPlugin(JsonApiErrorsPlugin):
661 ''' Json javascript wrapper. '''
662 name = 'jsonp'
663 api = 2
664
665 def __init__(self, **kw):
666 super(JsonpPlugin, self).__init__(**kw)
667
668 @staticmethod
669 def to_jsonp(json):
670 jwrapper = request.query.callback or None
671 if(jwrapper):
672 response.set_header('Content-Type', 'application/javascript')
673 json = jwrapper + '(' + json + ');'
674 return json
675
676 def apply(self, callback, route):
677 def wrap(*a, **kw):
678 return self.to_jsonp(callback(*a, **kw))
679 return wrap
680
681 def json_errors(self, res, error):
682 json = super(JsonpPlugin, self).json_errors(res, error)
683 return self.to_jsonp(json)
684
685
Brad Bishop2c6fc762016-08-29 15:53:25 -0400686class App(Bottle):
Brad Bishop2ddfa002016-08-29 15:11:55 -0400687 def __init__(self):
Brad Bishop2c6fc762016-08-29 15:53:25 -0400688 super(App, self).__init__(autojson=False)
Brad Bishop2ddfa002016-08-29 15:11:55 -0400689 self.bus = dbus.SystemBus()
690 self.mapper = obmc.mapper.Mapper(self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500691
Brad Bishop87b63c12016-03-18 14:47:51 -0400692 self.install_hooks()
693 self.install_plugins()
694 self.create_handlers()
695 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500696
Brad Bishop87b63c12016-03-18 14:47:51 -0400697 def install_plugins(self):
698 # install json api plugins
699 json_kw = {'indent': 2, 'sort_keys': True}
Brad Bishop87b63c12016-03-18 14:47:51 -0400700 self.install(AuthorizationPlugin())
Brad Bishop80fe37a2016-03-29 10:54:54 -0400701 self.install(JsonpPlugin(**json_kw))
702 self.install(JSONPlugin(**json_kw))
Brad Bishop87b63c12016-03-18 14:47:51 -0400703 self.install(JsonApiResponsePlugin())
704 self.install(JsonApiRequestPlugin())
705 self.install(JsonApiRequestTypePlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500706
Brad Bishop87b63c12016-03-18 14:47:51 -0400707 def install_hooks(self):
708 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 Bishop87b63c12016-03-18 14:47:51 -0400734 def custom_router_match(self, environ):
735 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
736 needed doesn't work for us since the instance rules match
737 everything. This monkey-patch lets the route handler figure
738 out which response is needed. This could be accomplished
739 with a hook but that would require calling the router match
740 function twice.
741 '''
742 route, args = self.real_router_match(environ)
743 if isinstance(route.callback, RouteHandler):
744 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500745
Brad Bishop87b63c12016-03-18 14:47:51 -0400746 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500747
Brad Bishop87b63c12016-03-18 14:47:51 -0400748 @staticmethod
749 def strip_extra_slashes():
750 path = request.environ['PATH_INFO']
751 trailing = ("", "/")[path[-1] == '/']
752 parts = filter(bool, path.split('/'))
753 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing