blob: d8307ee30d16a6b997c017eca0eca60a53fea4a3 [file] [log] [blame]
Brad Bishopaa65f6e2015-10-27 16:28:51 -04001#!/usr/bin/env python
2
Brad Bishop68caa1e2016-03-04 15:42:08 -05003# Contributors Listed Below - COPYRIGHT 2016
4# [+] International Business Machines Corp.
5#
6#
7# Licensed under the Apache License, Version 2.0 (the "License");
8# you may not use this file except in compliance with the License.
9# You may obtain a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS,
15# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
16# implied. See the License for the specific language governing
17# permissions and limitations under the License.
18
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050019import os
20import sys
Brad Bishopaa65f6e2015-10-27 16:28:51 -040021import dbus
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050022import dbus.exceptions
23import json
24import logging
25from xml.etree import ElementTree
26from rocket import Rocket
27from bottle import Bottle, abort, request, response, JSONPlugin, HTTPError
Brad Bishopb103d2d2016-03-04 16:19:14 -050028import obmc.utils.misc
29import obmc.utils.pathtree
30from obmc.dbuslib.introspection import IntrospectionNodeParser
31import obmc.mapper
Brad Bishop2f428582015-12-02 10:56:11 -050032import spwd
33import grp
34import crypt
Brad Bishopaa65f6e2015-10-27 16:28:51 -040035
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050036DBUS_UNKNOWN_INTERFACE = 'org.freedesktop.UnknownInterface'
Brad Bishopf4e74982016-04-01 14:53:05 -040037DBUS_UNKNOWN_INTERFACE_ERROR = 'org.freedesktop.DBus.Error.UnknownInterface'
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050038DBUS_UNKNOWN_METHOD = 'org.freedesktop.DBus.Error.UnknownMethod'
39DBUS_INVALID_ARGS = 'org.freedesktop.DBus.Error.InvalidArgs'
Brad Bishopd4578922015-12-02 11:10:36 -050040DBUS_TYPE_ERROR = 'org.freedesktop.DBus.Python.TypeError'
Brad Bishopaac521c2015-11-25 09:16:35 -050041DELETE_IFACE = 'org.openbmc.Object.Delete'
Brad Bishop9ee57c42015-11-03 14:59:29 -050042
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050043_4034_msg = "The specified %s cannot be %s: '%s'"
Brad Bishopaa65f6e2015-10-27 16:28:51 -040044
Brad Bishop87b63c12016-03-18 14:47:51 -040045
Brad Bishop2f428582015-12-02 10:56:11 -050046def valid_user(session, *a, **kw):
Brad Bishop87b63c12016-03-18 14:47:51 -040047 ''' Authorization plugin callback that checks
48 that the user is logged in. '''
49 if session is None:
50 abort(403, 'Login required')
51
Brad Bishop2f428582015-12-02 10:56:11 -050052
53class UserInGroup:
Brad Bishop87b63c12016-03-18 14:47:51 -040054 ''' Authorization plugin callback that checks that the user is logged in
55 and a member of a group. '''
56 def __init__(self, group):
57 self.group = group
Brad Bishop2f428582015-12-02 10:56:11 -050058
Brad Bishop87b63c12016-03-18 14:47:51 -040059 def __call__(self, session, *a, **kw):
60 valid_user(session, *a, **kw)
61 res = False
Brad Bishop2f428582015-12-02 10:56:11 -050062
Brad Bishop87b63c12016-03-18 14:47:51 -040063 try:
64 res = session['user'] in grp.getgrnam(self.group)[3]
65 except KeyError:
66 pass
Brad Bishop2f428582015-12-02 10:56:11 -050067
Brad Bishop87b63c12016-03-18 14:47:51 -040068 if not res:
69 abort(403, 'Insufficient access')
70
Brad Bishop2f428582015-12-02 10:56:11 -050071
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050072class RouteHandler(object):
Brad Bishop6d190602016-04-15 13:09:39 -040073 _require_auth = obmc.utils.misc.makelist(valid_user)
Brad Bishopaa65f6e2015-10-27 16:28:51 -040074
Brad Bishop87b63c12016-03-18 14:47:51 -040075 def __init__(self, app, bus, verbs, rules):
76 self.app = app
77 self.bus = bus
Brad Bishopb103d2d2016-03-04 16:19:14 -050078 self.mapper = obmc.mapper.Mapper(bus)
Brad Bishop6d190602016-04-15 13:09:39 -040079 self._verbs = obmc.utils.misc.makelist(verbs)
Brad Bishop87b63c12016-03-18 14:47:51 -040080 self._rules = rules
Brad Bishop0f79e522016-03-18 13:33:17 -040081 self.intf_match = obmc.utils.misc.org_dot_openbmc_match
Brad Bishopaa65f6e2015-10-27 16:28:51 -040082
Brad Bishop87b63c12016-03-18 14:47:51 -040083 def _setup(self, **kw):
84 request.route_data = {}
85 if request.method in self._verbs:
86 return self.setup(**kw)
87 else:
88 self.find(**kw)
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 Bishop87b63c12016-03-18 14:47:51 -040095 def install(self):
96 self.app.route(
97 self._rules, callback=self,
98 method=['GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
Brad Bishopaa65f6e2015-10-27 16:28:51 -040099
Brad Bishop87b63c12016-03-18 14:47:51 -0400100 @staticmethod
101 def try_mapper_call(f, callback=None, **kw):
102 try:
103 return f(**kw)
104 except dbus.exceptions.DBusException, e:
Brad Bishopb103d2d2016-03-04 16:19:14 -0500105 if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
Brad Bishop87b63c12016-03-18 14:47:51 -0400106 raise
107 if callback is None:
108 def callback(e, **kw):
109 abort(404, str(e))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400110
Brad Bishop87b63c12016-03-18 14:47:51 -0400111 callback(e, **kw)
112
113 @staticmethod
114 def try_properties_interface(f, *a):
115 try:
116 return f(*a)
117 except dbus.exceptions.DBusException, e:
118 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_message():
119 # interface doesn't have any properties
120 return None
Brad Bishopf4e74982016-04-01 14:53:05 -0400121 if DBUS_UNKNOWN_INTERFACE_ERROR in e.get_dbus_name():
122 # interface doesn't have any properties
123 return None
Brad Bishop87b63c12016-03-18 14:47:51 -0400124 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
125 # properties interface not implemented at all
126 return None
127 raise
128
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400129
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500130class DirectoryHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400131 verbs = 'GET'
132 rules = '<path:path>/'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400133
Brad Bishop87b63c12016-03-18 14:47:51 -0400134 def __init__(self, app, bus):
135 super(DirectoryHandler, self).__init__(
136 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400137
Brad Bishop87b63c12016-03-18 14:47:51 -0400138 def find(self, path='/'):
139 return self.try_mapper_call(
140 self.mapper.get_subtree_paths, path=path, depth=1)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400141
Brad Bishop87b63c12016-03-18 14:47:51 -0400142 def setup(self, path='/'):
143 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400144
Brad Bishop87b63c12016-03-18 14:47:51 -0400145 def do_get(self, path='/'):
146 return request.route_data['map']
147
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400148
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500149class ListNamesHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400150 verbs = 'GET'
151 rules = ['/list', '<path:path>/list']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400152
Brad Bishop87b63c12016-03-18 14:47:51 -0400153 def __init__(self, app, bus):
154 super(ListNamesHandler, self).__init__(
155 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400156
Brad Bishop87b63c12016-03-18 14:47:51 -0400157 def find(self, path='/'):
158 return self.try_mapper_call(
159 self.mapper.get_subtree, path=path).keys()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400160
Brad Bishop87b63c12016-03-18 14:47:51 -0400161 def setup(self, path='/'):
162 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400163
Brad Bishop87b63c12016-03-18 14:47:51 -0400164 def do_get(self, path='/'):
165 return request.route_data['map']
166
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400167
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500168class ListHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400169 verbs = 'GET'
170 rules = ['/enumerate', '<path:path>/enumerate']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400171
Brad Bishop87b63c12016-03-18 14:47:51 -0400172 def __init__(self, app, bus):
173 super(ListHandler, self).__init__(
174 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400175
Brad Bishop87b63c12016-03-18 14:47:51 -0400176 def find(self, path='/'):
177 return self.try_mapper_call(
178 self.mapper.get_subtree, path=path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400179
Brad Bishop87b63c12016-03-18 14:47:51 -0400180 def setup(self, path='/'):
181 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400182
Brad Bishop87b63c12016-03-18 14:47:51 -0400183 def do_get(self, path='/'):
Brad Bishop71527b42016-04-01 14:51:14 -0400184 return {x: y for x, y in self.mapper.enumerate_subtree(
185 path,
186 mapper_data=request.route_data['map']).dataitems()}
Brad Bishop87b63c12016-03-18 14:47:51 -0400187
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400188
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500189class MethodHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400190 verbs = 'POST'
191 rules = '<path:path>/action/<method>'
192 request_type = list
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400193
Brad Bishop87b63c12016-03-18 14:47:51 -0400194 def __init__(self, app, bus):
195 super(MethodHandler, self).__init__(
196 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400197
Brad Bishop87b63c12016-03-18 14:47:51 -0400198 def find(self, path, method):
199 busses = self.try_mapper_call(
200 self.mapper.get_object, path=path)
201 for items in busses.iteritems():
202 m = self.find_method_on_bus(path, method, *items)
203 if m:
204 return m
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400205
Brad Bishop87b63c12016-03-18 14:47:51 -0400206 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400207
Brad Bishop87b63c12016-03-18 14:47:51 -0400208 def setup(self, path, method):
209 request.route_data['method'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400210
Brad Bishop87b63c12016-03-18 14:47:51 -0400211 def do_post(self, path, method):
212 try:
213 if request.parameter_list:
214 return request.route_data['method'](*request.parameter_list)
215 else:
216 return request.route_data['method']()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400217
Brad Bishop87b63c12016-03-18 14:47:51 -0400218 except dbus.exceptions.DBusException, e:
219 if e.get_dbus_name() == DBUS_INVALID_ARGS:
220 abort(400, str(e))
221 if e.get_dbus_name() == DBUS_TYPE_ERROR:
222 abort(400, str(e))
223 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400224
Brad Bishop87b63c12016-03-18 14:47:51 -0400225 @staticmethod
226 def find_method_in_interface(method, obj, interface, methods):
227 if methods is None:
228 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400229
Brad Bishop6d190602016-04-15 13:09:39 -0400230 method = obmc.utils.misc.find_case_insensitive(method, methods.keys())
Brad Bishop87b63c12016-03-18 14:47:51 -0400231 if method is not None:
232 iface = dbus.Interface(obj, interface)
233 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400234
Brad Bishop87b63c12016-03-18 14:47:51 -0400235 def find_method_on_bus(self, path, method, bus, interfaces):
236 obj = self.bus.get_object(bus, path, introspect=False)
237 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
238 data = iface.Introspect()
239 parser = IntrospectionNodeParser(
240 ElementTree.fromstring(data),
Brad Bishopb103d2d2016-03-04 16:19:14 -0500241 intf_match=obmc.utils.misc.ListMatch(interfaces))
Brad Bishop87b63c12016-03-18 14:47:51 -0400242 for x, y in parser.get_interfaces().iteritems():
243 m = self.find_method_in_interface(
244 method, obj, x, y.get('method'))
245 if m:
246 return m
247
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400248
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500249class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400250 verbs = ['PUT', 'GET']
251 rules = '<path:path>/attr/<prop>'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400252
Brad Bishop87b63c12016-03-18 14:47:51 -0400253 def __init__(self, app, bus):
254 super(PropertyHandler, self).__init__(
255 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400256
Brad Bishop87b63c12016-03-18 14:47:51 -0400257 def find(self, path, prop):
258 self.app.instance_handler.setup(path)
259 obj = self.app.instance_handler.do_get(path)
260 try:
261 obj[prop]
262 except KeyError, e:
263 if request.method == 'PUT':
264 abort(403, _4034_msg % ('property', 'created', str(e)))
265 else:
266 abort(404, _4034_msg % ('property', 'found', str(e)))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400267
Brad Bishop87b63c12016-03-18 14:47:51 -0400268 return {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500269
Brad Bishop87b63c12016-03-18 14:47:51 -0400270 def setup(self, path, prop):
271 request.route_data['obj'] = self.find(path, prop)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500272
Brad Bishop87b63c12016-03-18 14:47:51 -0400273 def do_get(self, path, prop):
274 return request.route_data['obj'][path][prop]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500275
Brad Bishop87b63c12016-03-18 14:47:51 -0400276 def do_put(self, path, prop, value=None):
277 if value is None:
278 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500279
Brad Bishop87b63c12016-03-18 14:47:51 -0400280 prop, iface, properties_iface = self.get_host_interface(
281 path, prop, request.route_data['map'][path])
282 try:
283 properties_iface.Set(iface, prop, value)
284 except ValueError, e:
285 abort(400, str(e))
286 except dbus.exceptions.DBusException, e:
287 if e.get_dbus_name() == DBUS_INVALID_ARGS:
288 abort(403, str(e))
289 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500290
Brad Bishop87b63c12016-03-18 14:47:51 -0400291 def get_host_interface(self, path, prop, bus_info):
292 for bus, interfaces in bus_info.iteritems():
293 obj = self.bus.get_object(bus, path, introspect=True)
294 properties_iface = dbus.Interface(
295 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500296
Brad Bishop87b63c12016-03-18 14:47:51 -0400297 info = self.get_host_interface_on_bus(
298 path, prop, properties_iface, bus, interfaces)
299 if info is not None:
300 prop, iface = info
301 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500302
Brad Bishop87b63c12016-03-18 14:47:51 -0400303 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
304 for i in interfaces:
305 properties = self.try_properties_interface(iface.GetAll, i)
306 if properties is None:
307 continue
Brad Bishop6d190602016-04-15 13:09:39 -0400308 prop = obmc.utils.misc.find_case_insensitive(prop, properties.keys())
Brad Bishop87b63c12016-03-18 14:47:51 -0400309 if prop is None:
310 continue
311 return prop, i
312
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500313
Brad Bishop2503bd62015-12-16 17:56:12 -0500314class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400315 verbs = ['GET']
316 rules = '<path:path>/schema'
Brad Bishop2503bd62015-12-16 17:56:12 -0500317
Brad Bishop87b63c12016-03-18 14:47:51 -0400318 def __init__(self, app, bus):
319 super(SchemaHandler, self).__init__(
320 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500321
Brad Bishop87b63c12016-03-18 14:47:51 -0400322 def find(self, path):
323 return self.try_mapper_call(
324 self.mapper.get_object,
325 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500326
Brad Bishop87b63c12016-03-18 14:47:51 -0400327 def setup(self, path):
328 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500329
Brad Bishop87b63c12016-03-18 14:47:51 -0400330 def do_get(self, path):
331 schema = {}
332 for x in request.route_data['map'].iterkeys():
333 obj = self.bus.get_object(x, path, introspect=False)
334 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
335 data = iface.Introspect()
336 parser = IntrospectionNodeParser(
337 ElementTree.fromstring(data))
338 for x, y in parser.get_interfaces().iteritems():
339 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500340
Brad Bishop87b63c12016-03-18 14:47:51 -0400341 return schema
342
Brad Bishop2503bd62015-12-16 17:56:12 -0500343
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500344class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400345 verbs = ['GET', 'PUT', 'DELETE']
346 rules = '<path:path>'
347 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500348
Brad Bishop87b63c12016-03-18 14:47:51 -0400349 def __init__(self, app, bus):
350 super(InstanceHandler, self).__init__(
351 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500352
Brad Bishop87b63c12016-03-18 14:47:51 -0400353 def find(self, path, callback=None):
354 return {path: self.try_mapper_call(
355 self.mapper.get_object,
356 callback,
357 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500358
Brad Bishop87b63c12016-03-18 14:47:51 -0400359 def setup(self, path):
360 callback = None
361 if request.method == 'PUT':
362 def callback(e, **kw):
363 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500364
Brad Bishop87b63c12016-03-18 14:47:51 -0400365 if request.route_data.get('map') is None:
366 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500367
Brad Bishop87b63c12016-03-18 14:47:51 -0400368 def do_get(self, path):
Brad Bishop71527b42016-04-01 14:51:14 -0400369 return self.mapper.enumerate_object(
370 path,
371 mapper_data=request.route_data['map'])
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500372
Brad Bishop87b63c12016-03-18 14:47:51 -0400373 def do_put(self, path):
374 # make sure all properties exist in the request
375 obj = set(self.do_get(path).keys())
376 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500377
Brad Bishop87b63c12016-03-18 14:47:51 -0400378 diff = list(obj.difference(req))
379 if diff:
380 abort(403, _4034_msg % (
381 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500382
Brad Bishop87b63c12016-03-18 14:47:51 -0400383 diff = list(req.difference(obj))
384 if diff:
385 abort(403, _4034_msg % (
386 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500387
Brad Bishop87b63c12016-03-18 14:47:51 -0400388 for p, v in request.parameter_list.iteritems():
389 self.app.property_handler.do_put(
390 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500391
Brad Bishop87b63c12016-03-18 14:47:51 -0400392 def do_delete(self, path):
393 for bus_info in request.route_data['map'][path].iteritems():
394 if self.bus_missing_delete(path, *bus_info):
395 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500396
Brad Bishop87b63c12016-03-18 14:47:51 -0400397 for bus in request.route_data['map'][path].iterkeys():
398 self.delete_on_bus(path, bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500399
Brad Bishop87b63c12016-03-18 14:47:51 -0400400 def bus_missing_delete(self, path, bus, interfaces):
401 return DELETE_IFACE not in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500402
Brad Bishop87b63c12016-03-18 14:47:51 -0400403 def delete_on_bus(self, path, bus):
404 obj = self.bus.get_object(bus, path, introspect=False)
405 delete_iface = dbus.Interface(
406 obj, dbus_interface=DELETE_IFACE)
407 delete_iface.Delete()
408
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500409
Brad Bishop2f428582015-12-02 10:56:11 -0500410class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400411 ''' Handles the /login and /logout routes, manages
412 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500413
Brad Bishop87b63c12016-03-18 14:47:51 -0400414 rules = ['/login', '/logout']
415 login_str = "User '%s' logged %s"
416 bad_passwd_str = "Invalid username or password"
417 no_user_str = "No user logged in"
418 bad_json_str = "Expecting request format { 'data': " \
419 "[<username>, <password>] }, got '%s'"
420 _require_auth = None
421 MAX_SESSIONS = 16
Brad Bishop2f428582015-12-02 10:56:11 -0500422
Brad Bishop87b63c12016-03-18 14:47:51 -0400423 def __init__(self, app, bus):
424 super(SessionHandler, self).__init__(
425 app, bus)
426 self.hmac_key = os.urandom(128)
427 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500428
Brad Bishop87b63c12016-03-18 14:47:51 -0400429 @staticmethod
430 def authenticate(username, clear):
431 try:
432 encoded = spwd.getspnam(username)[1]
433 return encoded == crypt.crypt(clear, encoded)
434 except KeyError:
435 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500436
Brad Bishop87b63c12016-03-18 14:47:51 -0400437 def invalidate_session(self, session):
438 try:
439 self.session_store.remove(session)
440 except ValueError:
441 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500442
Brad Bishop87b63c12016-03-18 14:47:51 -0400443 def new_session(self):
444 sid = os.urandom(32)
445 if self.MAX_SESSIONS <= len(self.session_store):
446 self.session_store.pop()
447 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500448
Brad Bishop87b63c12016-03-18 14:47:51 -0400449 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500450
Brad Bishop87b63c12016-03-18 14:47:51 -0400451 def get_session(self, sid):
452 sids = [x['sid'] for x in self.session_store]
453 try:
454 return self.session_store[sids.index(sid)]
455 except ValueError:
456 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500457
Brad Bishop87b63c12016-03-18 14:47:51 -0400458 def get_session_from_cookie(self):
459 return self.get_session(
460 request.get_cookie(
461 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500462
Brad Bishop87b63c12016-03-18 14:47:51 -0400463 def do_post(self, **kw):
464 if request.path == '/login':
465 return self.do_login(**kw)
466 else:
467 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500468
Brad Bishop87b63c12016-03-18 14:47:51 -0400469 def do_logout(self, **kw):
470 session = self.get_session_from_cookie()
471 if session is not None:
472 user = session['user']
473 self.invalidate_session(session)
474 response.delete_cookie('sid')
475 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500476
Brad Bishop87b63c12016-03-18 14:47:51 -0400477 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500478
Brad Bishop87b63c12016-03-18 14:47:51 -0400479 def do_login(self, **kw):
480 session = self.get_session_from_cookie()
481 if session is not None:
482 return self.login_str % (session['user'], 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500483
Brad Bishop87b63c12016-03-18 14:47:51 -0400484 if len(request.parameter_list) != 2:
485 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500486
Brad Bishop87b63c12016-03-18 14:47:51 -0400487 if not self.authenticate(*request.parameter_list):
488 return self.bad_passwd_str
Brad Bishop2f428582015-12-02 10:56:11 -0500489
Brad Bishop87b63c12016-03-18 14:47:51 -0400490 user = request.parameter_list[0]
491 session = self.new_session()
492 session['user'] = user
493 response.set_cookie(
494 'sid', session['sid'], secret=self.hmac_key,
495 secure=True,
496 httponly=True)
497 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500498
Brad Bishop87b63c12016-03-18 14:47:51 -0400499 def find(self, **kw):
500 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500501
Brad Bishop87b63c12016-03-18 14:47:51 -0400502 def setup(self, **kw):
503 pass
504
Brad Bishop2f428582015-12-02 10:56:11 -0500505
506class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400507 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500508
Brad Bishop87b63c12016-03-18 14:47:51 -0400509 name = 'authorization'
510 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -0500511
Brad Bishop87b63c12016-03-18 14:47:51 -0400512 class Compose:
513 def __init__(self, validators, callback, session_mgr):
514 self.validators = validators
515 self.callback = callback
516 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -0500517
Brad Bishop87b63c12016-03-18 14:47:51 -0400518 def __call__(self, *a, **kw):
519 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
520 session = self.session_mgr.get_session(sid)
521 for x in self.validators:
522 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500523
Brad Bishop87b63c12016-03-18 14:47:51 -0400524 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500525
Brad Bishop87b63c12016-03-18 14:47:51 -0400526 def apply(self, callback, route):
527 undecorated = route.get_undecorated_callback()
528 if not isinstance(undecorated, RouteHandler):
529 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500530
Brad Bishop87b63c12016-03-18 14:47:51 -0400531 auth_types = getattr(
532 undecorated, '_require_auth', None)
533 if not auth_types:
534 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500535
Brad Bishop87b63c12016-03-18 14:47:51 -0400536 return self.Compose(
537 auth_types, callback, undecorated.app.session_handler)
538
Brad Bishop2f428582015-12-02 10:56:11 -0500539
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500540class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400541 ''' Ensures request content satisfies the OpenBMC json api format. '''
542 name = 'json_api_request'
543 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500544
Brad Bishop87b63c12016-03-18 14:47:51 -0400545 error_str = "Expecting request format { 'data': <value> }, got '%s'"
546 type_error_str = "Unsupported Content-Type: '%s'"
547 json_type = "application/json"
548 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500549
Brad Bishop87b63c12016-03-18 14:47:51 -0400550 @staticmethod
551 def content_expected():
552 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500553
Brad Bishop87b63c12016-03-18 14:47:51 -0400554 def validate_request(self):
555 if request.content_length > 0 and \
556 request.content_type != self.json_type:
557 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500558
Brad Bishop87b63c12016-03-18 14:47:51 -0400559 try:
560 request.parameter_list = request.json.get('data')
561 except ValueError, e:
562 abort(400, str(e))
563 except (AttributeError, KeyError, TypeError):
564 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500565
Brad Bishop87b63c12016-03-18 14:47:51 -0400566 def apply(self, callback, route):
567 verbs = getattr(
568 route.get_undecorated_callback(), '_verbs', None)
569 if verbs is None:
570 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500571
Brad Bishop87b63c12016-03-18 14:47:51 -0400572 if not set(self.request_methods).intersection(verbs):
573 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500574
Brad Bishop87b63c12016-03-18 14:47:51 -0400575 def wrap(*a, **kw):
576 if self.content_expected():
577 self.validate_request()
578 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500579
Brad Bishop87b63c12016-03-18 14:47:51 -0400580 return wrap
581
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500582
583class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400584 ''' Ensures request content type satisfies the OpenBMC json api format. '''
585 name = 'json_api_method_request'
586 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500587
Brad Bishop87b63c12016-03-18 14:47:51 -0400588 error_str = "Expecting request format { 'data': %s }, got '%s'"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500589
Brad Bishop87b63c12016-03-18 14:47:51 -0400590 def apply(self, callback, route):
591 request_type = getattr(
592 route.get_undecorated_callback(), 'request_type', None)
593 if request_type is None:
594 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500595
Brad Bishop87b63c12016-03-18 14:47:51 -0400596 def validate_request():
597 if not isinstance(request.parameter_list, request_type):
598 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500599
Brad Bishop87b63c12016-03-18 14:47:51 -0400600 def wrap(*a, **kw):
601 if JsonApiRequestPlugin.content_expected():
602 validate_request()
603 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500604
Brad Bishop87b63c12016-03-18 14:47:51 -0400605 return wrap
606
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500607
608class JsonApiResponsePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400609 ''' Emits normal responses in the OpenBMC json api format. '''
610 name = 'json_api_response'
611 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500612
Brad Bishop87b63c12016-03-18 14:47:51 -0400613 def apply(self, callback, route):
614 def wrap(*a, **kw):
615 resp = {'data': callback(*a, **kw)}
616 resp['status'] = 'ok'
617 resp['message'] = response.status_line
618 return resp
619 return wrap
620
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500621
622class JsonApiErrorsPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400623 ''' Emits error responses in the OpenBMC json api format. '''
624 name = 'json_api_errors'
625 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500626
Brad Bishop87b63c12016-03-18 14:47:51 -0400627 def __init__(self, **kw):
628 self.app = None
629 self.function_type = None
630 self.original = None
631 self.json_opts = {
632 x: y for x, y in kw.iteritems()
633 if x in ['indent', 'sort_keys']}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500634
Brad Bishop87b63c12016-03-18 14:47:51 -0400635 def setup(self, app):
636 self.app = app
637 self.function_type = type(app.default_error_handler)
638 self.original = app.default_error_handler
639 self.app.default_error_handler = self.function_type(
640 self.json_errors, app, Bottle)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500641
Brad Bishop87b63c12016-03-18 14:47:51 -0400642 def apply(self, callback, route):
643 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500644
Brad Bishop87b63c12016-03-18 14:47:51 -0400645 def close(self):
646 self.app.default_error_handler = self.function_type(
647 self.original, self.app, Bottle)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500648
Brad Bishop87b63c12016-03-18 14:47:51 -0400649 def json_errors(self, res, error):
650 response_object = {'status': 'error', 'data': {}}
651 response_object['message'] = error.status_line
652 response_object['data']['description'] = str(error.body)
653 if error.status_code == 500:
654 response_object['data']['exception'] = repr(error.exception)
655 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500656
Brad Bishop87b63c12016-03-18 14:47:51 -0400657 json_response = json.dumps(response_object, **self.json_opts)
658 response.content_type = 'application/json'
659 return json_response
660
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500661
Brad Bishop80fe37a2016-03-29 10:54:54 -0400662class JsonpPlugin(JsonApiErrorsPlugin):
663 ''' Json javascript wrapper. '''
664 name = 'jsonp'
665 api = 2
666
667 def __init__(self, **kw):
668 super(JsonpPlugin, self).__init__(**kw)
669
670 @staticmethod
671 def to_jsonp(json):
672 jwrapper = request.query.callback or None
673 if(jwrapper):
674 response.set_header('Content-Type', 'application/javascript')
675 json = jwrapper + '(' + json + ');'
676 return json
677
678 def apply(self, callback, route):
679 def wrap(*a, **kw):
680 return self.to_jsonp(callback(*a, **kw))
681 return wrap
682
683 def json_errors(self, res, error):
684 json = super(JsonpPlugin, self).json_errors(res, error)
685 return self.to_jsonp(json)
686
687
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500688class RestApp(Bottle):
Brad Bishop2ddfa002016-08-29 15:11:55 -0400689 def __init__(self):
Brad Bishop87b63c12016-03-18 14:47:51 -0400690 super(RestApp, self).__init__(autojson=False)
Brad Bishop2ddfa002016-08-29 15:11:55 -0400691 self.bus = dbus.SystemBus()
692 self.mapper = obmc.mapper.Mapper(self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500693
Brad Bishop87b63c12016-03-18 14:47:51 -0400694 self.install_hooks()
695 self.install_plugins()
696 self.create_handlers()
697 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500698
Brad Bishop87b63c12016-03-18 14:47:51 -0400699 def install_plugins(self):
700 # install json api plugins
701 json_kw = {'indent': 2, 'sort_keys': True}
Brad Bishop87b63c12016-03-18 14:47:51 -0400702 self.install(AuthorizationPlugin())
Brad Bishop80fe37a2016-03-29 10:54:54 -0400703 self.install(JsonpPlugin(**json_kw))
704 self.install(JSONPlugin(**json_kw))
Brad Bishop87b63c12016-03-18 14:47:51 -0400705 self.install(JsonApiResponsePlugin())
706 self.install(JsonApiRequestPlugin())
707 self.install(JsonApiRequestTypePlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500708
Brad Bishop87b63c12016-03-18 14:47:51 -0400709 def install_hooks(self):
710 self.real_router_match = self.router.match
711 self.router.match = self.custom_router_match
712 self.add_hook('before_request', self.strip_extra_slashes)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500713
Brad Bishop87b63c12016-03-18 14:47:51 -0400714 def create_handlers(self):
715 # create route handlers
716 self.session_handler = SessionHandler(self, self.bus)
717 self.directory_handler = DirectoryHandler(self, self.bus)
718 self.list_names_handler = ListNamesHandler(self, self.bus)
719 self.list_handler = ListHandler(self, self.bus)
720 self.method_handler = MethodHandler(self, self.bus)
721 self.property_handler = PropertyHandler(self, self.bus)
722 self.schema_handler = SchemaHandler(self, self.bus)
723 self.instance_handler = InstanceHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500724
Brad Bishop87b63c12016-03-18 14:47:51 -0400725 def install_handlers(self):
726 self.session_handler.install()
727 self.directory_handler.install()
728 self.list_names_handler.install()
729 self.list_handler.install()
730 self.method_handler.install()
731 self.property_handler.install()
732 self.schema_handler.install()
733 # this has to come last, since it matches everything
734 self.instance_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500735
Brad Bishop87b63c12016-03-18 14:47:51 -0400736 def custom_router_match(self, environ):
737 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
738 needed doesn't work for us since the instance rules match
739 everything. This monkey-patch lets the route handler figure
740 out which response is needed. This could be accomplished
741 with a hook but that would require calling the router match
742 function twice.
743 '''
744 route, args = self.real_router_match(environ)
745 if isinstance(route.callback, RouteHandler):
746 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500747
Brad Bishop87b63c12016-03-18 14:47:51 -0400748 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500749
Brad Bishop87b63c12016-03-18 14:47:51 -0400750 @staticmethod
751 def strip_extra_slashes():
752 path = request.environ['PATH_INFO']
753 trailing = ("", "/")[path[-1] == '/']
754 parts = filter(bool, path.split('/'))
755 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400756
757if __name__ == '__main__':
Brad Bishop87b63c12016-03-18 14:47:51 -0400758 log = logging.getLogger('Rocket.Errors')
759 log.setLevel(logging.INFO)
760 log.addHandler(logging.StreamHandler(sys.stdout))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500761
Brad Bishop2ddfa002016-08-29 15:11:55 -0400762 app = RestApp()
Brad Bishop87b63c12016-03-18 14:47:51 -0400763 default_cert = os.path.join(
764 sys.prefix, 'share', os.path.basename(__file__), 'cert.pem')
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500765
Brad Bishop87b63c12016-03-18 14:47:51 -0400766 server = Rocket(
767 ('0.0.0.0', 443, default_cert, default_cert),
768 'wsgi', {'wsgi_app': app},
769 min_threads=1,
770 max_threads=1)
771 server.start()