blob: 226b1b7bb4796ef97751152ff075a3f014076992 [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 -050072def find_case_insensitive(value, lst):
Brad Bishop87b63c12016-03-18 14:47:51 -040073 return next((x for x in lst if x.lower() == value.lower()), None)
74
Brad Bishopaa65f6e2015-10-27 16:28:51 -040075
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050076def makelist(data):
Brad Bishop87b63c12016-03-18 14:47:51 -040077 if isinstance(data, list):
78 return data
79 elif data:
80 return [data]
81 else:
82 return []
83
Brad Bishopaa65f6e2015-10-27 16:28:51 -040084
Brad Bishopb1cbdaf2015-11-13 21:28:16 -050085class RouteHandler(object):
Brad Bishop87b63c12016-03-18 14:47:51 -040086 _require_auth = makelist(valid_user)
Brad Bishopaa65f6e2015-10-27 16:28:51 -040087
Brad Bishop87b63c12016-03-18 14:47:51 -040088 def __init__(self, app, bus, verbs, rules):
89 self.app = app
90 self.bus = bus
Brad Bishopb103d2d2016-03-04 16:19:14 -050091 self.mapper = obmc.mapper.Mapper(bus)
Brad Bishop87b63c12016-03-18 14:47:51 -040092 self._verbs = makelist(verbs)
93 self._rules = rules
Brad Bishop0f79e522016-03-18 13:33:17 -040094 self.intf_match = obmc.utils.misc.org_dot_openbmc_match
Brad Bishopaa65f6e2015-10-27 16:28:51 -040095
Brad Bishop87b63c12016-03-18 14:47:51 -040096 def _setup(self, **kw):
97 request.route_data = {}
98 if request.method in self._verbs:
99 return self.setup(**kw)
100 else:
101 self.find(**kw)
102 raise HTTPError(
103 405, "Method not allowed.", Allow=','.join(self._verbs))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400104
Brad Bishop87b63c12016-03-18 14:47:51 -0400105 def __call__(self, **kw):
106 return getattr(self, 'do_' + request.method.lower())(**kw)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400107
Brad Bishop87b63c12016-03-18 14:47:51 -0400108 def install(self):
109 self.app.route(
110 self._rules, callback=self,
111 method=['GET', 'PUT', 'PATCH', 'POST', 'DELETE'])
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400112
Brad Bishop87b63c12016-03-18 14:47:51 -0400113 @staticmethod
114 def try_mapper_call(f, callback=None, **kw):
115 try:
116 return f(**kw)
117 except dbus.exceptions.DBusException, e:
Brad Bishopb103d2d2016-03-04 16:19:14 -0500118 if e.get_dbus_name() != obmc.mapper.MAPPER_NOT_FOUND:
Brad Bishop87b63c12016-03-18 14:47:51 -0400119 raise
120 if callback is None:
121 def callback(e, **kw):
122 abort(404, str(e))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400123
Brad Bishop87b63c12016-03-18 14:47:51 -0400124 callback(e, **kw)
125
126 @staticmethod
127 def try_properties_interface(f, *a):
128 try:
129 return f(*a)
130 except dbus.exceptions.DBusException, e:
131 if DBUS_UNKNOWN_INTERFACE in e.get_dbus_message():
132 # interface doesn't have any properties
133 return None
Brad Bishopf4e74982016-04-01 14:53:05 -0400134 if DBUS_UNKNOWN_INTERFACE_ERROR in e.get_dbus_name():
135 # interface doesn't have any properties
136 return None
Brad Bishop87b63c12016-03-18 14:47:51 -0400137 if DBUS_UNKNOWN_METHOD == e.get_dbus_name():
138 # properties interface not implemented at all
139 return None
140 raise
141
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400142
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500143class DirectoryHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400144 verbs = 'GET'
145 rules = '<path:path>/'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400146
Brad Bishop87b63c12016-03-18 14:47:51 -0400147 def __init__(self, app, bus):
148 super(DirectoryHandler, self).__init__(
149 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400150
Brad Bishop87b63c12016-03-18 14:47:51 -0400151 def find(self, path='/'):
152 return self.try_mapper_call(
153 self.mapper.get_subtree_paths, path=path, depth=1)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400154
Brad Bishop87b63c12016-03-18 14:47:51 -0400155 def setup(self, path='/'):
156 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400157
Brad Bishop87b63c12016-03-18 14:47:51 -0400158 def do_get(self, path='/'):
159 return request.route_data['map']
160
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400161
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500162class ListNamesHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400163 verbs = 'GET'
164 rules = ['/list', '<path:path>/list']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400165
Brad Bishop87b63c12016-03-18 14:47:51 -0400166 def __init__(self, app, bus):
167 super(ListNamesHandler, self).__init__(
168 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400169
Brad Bishop87b63c12016-03-18 14:47:51 -0400170 def find(self, path='/'):
171 return self.try_mapper_call(
172 self.mapper.get_subtree, path=path).keys()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400173
Brad Bishop87b63c12016-03-18 14:47:51 -0400174 def setup(self, path='/'):
175 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400176
Brad Bishop87b63c12016-03-18 14:47:51 -0400177 def do_get(self, path='/'):
178 return request.route_data['map']
179
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400180
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500181class ListHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400182 verbs = 'GET'
183 rules = ['/enumerate', '<path:path>/enumerate']
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400184
Brad Bishop87b63c12016-03-18 14:47:51 -0400185 def __init__(self, app, bus):
186 super(ListHandler, self).__init__(
187 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400188
Brad Bishop87b63c12016-03-18 14:47:51 -0400189 def find(self, path='/'):
190 return self.try_mapper_call(
191 self.mapper.get_subtree, path=path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400192
Brad Bishop87b63c12016-03-18 14:47:51 -0400193 def setup(self, path='/'):
194 request.route_data['map'] = self.find(path)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400195
Brad Bishop87b63c12016-03-18 14:47:51 -0400196 def do_get(self, path='/'):
Brad Bishop71527b42016-04-01 14:51:14 -0400197 return {x: y for x, y in self.mapper.enumerate_subtree(
198 path,
199 mapper_data=request.route_data['map']).dataitems()}
Brad Bishop87b63c12016-03-18 14:47:51 -0400200
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400201
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500202class MethodHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400203 verbs = 'POST'
204 rules = '<path:path>/action/<method>'
205 request_type = list
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400206
Brad Bishop87b63c12016-03-18 14:47:51 -0400207 def __init__(self, app, bus):
208 super(MethodHandler, self).__init__(
209 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400210
Brad Bishop87b63c12016-03-18 14:47:51 -0400211 def find(self, path, method):
212 busses = self.try_mapper_call(
213 self.mapper.get_object, path=path)
214 for items in busses.iteritems():
215 m = self.find_method_on_bus(path, method, *items)
216 if m:
217 return m
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400218
Brad Bishop87b63c12016-03-18 14:47:51 -0400219 abort(404, _4034_msg % ('method', 'found', method))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400220
Brad Bishop87b63c12016-03-18 14:47:51 -0400221 def setup(self, path, method):
222 request.route_data['method'] = self.find(path, method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400223
Brad Bishop87b63c12016-03-18 14:47:51 -0400224 def do_post(self, path, method):
225 try:
226 if request.parameter_list:
227 return request.route_data['method'](*request.parameter_list)
228 else:
229 return request.route_data['method']()
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400230
Brad Bishop87b63c12016-03-18 14:47:51 -0400231 except dbus.exceptions.DBusException, e:
232 if e.get_dbus_name() == DBUS_INVALID_ARGS:
233 abort(400, str(e))
234 if e.get_dbus_name() == DBUS_TYPE_ERROR:
235 abort(400, str(e))
236 raise
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400237
Brad Bishop87b63c12016-03-18 14:47:51 -0400238 @staticmethod
239 def find_method_in_interface(method, obj, interface, methods):
240 if methods is None:
241 return None
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400242
Brad Bishop87b63c12016-03-18 14:47:51 -0400243 method = find_case_insensitive(method, methods.keys())
244 if method is not None:
245 iface = dbus.Interface(obj, interface)
246 return iface.get_dbus_method(method)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400247
Brad Bishop87b63c12016-03-18 14:47:51 -0400248 def find_method_on_bus(self, path, method, bus, interfaces):
249 obj = self.bus.get_object(bus, path, introspect=False)
250 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
251 data = iface.Introspect()
252 parser = IntrospectionNodeParser(
253 ElementTree.fromstring(data),
Brad Bishopb103d2d2016-03-04 16:19:14 -0500254 intf_match=obmc.utils.misc.ListMatch(interfaces))
Brad Bishop87b63c12016-03-18 14:47:51 -0400255 for x, y in parser.get_interfaces().iteritems():
256 m = self.find_method_in_interface(
257 method, obj, x, y.get('method'))
258 if m:
259 return m
260
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400261
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500262class PropertyHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400263 verbs = ['PUT', 'GET']
264 rules = '<path:path>/attr/<prop>'
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400265
Brad Bishop87b63c12016-03-18 14:47:51 -0400266 def __init__(self, app, bus):
267 super(PropertyHandler, self).__init__(
268 app, bus, self.verbs, self.rules)
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400269
Brad Bishop87b63c12016-03-18 14:47:51 -0400270 def find(self, path, prop):
271 self.app.instance_handler.setup(path)
272 obj = self.app.instance_handler.do_get(path)
273 try:
274 obj[prop]
275 except KeyError, e:
276 if request.method == 'PUT':
277 abort(403, _4034_msg % ('property', 'created', str(e)))
278 else:
279 abort(404, _4034_msg % ('property', 'found', str(e)))
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400280
Brad Bishop87b63c12016-03-18 14:47:51 -0400281 return {path: obj}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500282
Brad Bishop87b63c12016-03-18 14:47:51 -0400283 def setup(self, path, prop):
284 request.route_data['obj'] = self.find(path, prop)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500285
Brad Bishop87b63c12016-03-18 14:47:51 -0400286 def do_get(self, path, prop):
287 return request.route_data['obj'][path][prop]
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500288
Brad Bishop87b63c12016-03-18 14:47:51 -0400289 def do_put(self, path, prop, value=None):
290 if value is None:
291 value = request.parameter_list
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500292
Brad Bishop87b63c12016-03-18 14:47:51 -0400293 prop, iface, properties_iface = self.get_host_interface(
294 path, prop, request.route_data['map'][path])
295 try:
296 properties_iface.Set(iface, prop, value)
297 except ValueError, e:
298 abort(400, str(e))
299 except dbus.exceptions.DBusException, e:
300 if e.get_dbus_name() == DBUS_INVALID_ARGS:
301 abort(403, str(e))
302 raise
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500303
Brad Bishop87b63c12016-03-18 14:47:51 -0400304 def get_host_interface(self, path, prop, bus_info):
305 for bus, interfaces in bus_info.iteritems():
306 obj = self.bus.get_object(bus, path, introspect=True)
307 properties_iface = dbus.Interface(
308 obj, dbus_interface=dbus.PROPERTIES_IFACE)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500309
Brad Bishop87b63c12016-03-18 14:47:51 -0400310 info = self.get_host_interface_on_bus(
311 path, prop, properties_iface, bus, interfaces)
312 if info is not None:
313 prop, iface = info
314 return prop, iface, properties_iface
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500315
Brad Bishop87b63c12016-03-18 14:47:51 -0400316 def get_host_interface_on_bus(self, path, prop, iface, bus, interfaces):
317 for i in interfaces:
318 properties = self.try_properties_interface(iface.GetAll, i)
319 if properties is None:
320 continue
321 prop = find_case_insensitive(prop, properties.keys())
322 if prop is None:
323 continue
324 return prop, i
325
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500326
Brad Bishop2503bd62015-12-16 17:56:12 -0500327class SchemaHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400328 verbs = ['GET']
329 rules = '<path:path>/schema'
Brad Bishop2503bd62015-12-16 17:56:12 -0500330
Brad Bishop87b63c12016-03-18 14:47:51 -0400331 def __init__(self, app, bus):
332 super(SchemaHandler, self).__init__(
333 app, bus, self.verbs, self.rules)
Brad Bishop2503bd62015-12-16 17:56:12 -0500334
Brad Bishop87b63c12016-03-18 14:47:51 -0400335 def find(self, path):
336 return self.try_mapper_call(
337 self.mapper.get_object,
338 path=path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500339
Brad Bishop87b63c12016-03-18 14:47:51 -0400340 def setup(self, path):
341 request.route_data['map'] = self.find(path)
Brad Bishop2503bd62015-12-16 17:56:12 -0500342
Brad Bishop87b63c12016-03-18 14:47:51 -0400343 def do_get(self, path):
344 schema = {}
345 for x in request.route_data['map'].iterkeys():
346 obj = self.bus.get_object(x, path, introspect=False)
347 iface = dbus.Interface(obj, dbus.INTROSPECTABLE_IFACE)
348 data = iface.Introspect()
349 parser = IntrospectionNodeParser(
350 ElementTree.fromstring(data))
351 for x, y in parser.get_interfaces().iteritems():
352 schema[x] = y
Brad Bishop2503bd62015-12-16 17:56:12 -0500353
Brad Bishop87b63c12016-03-18 14:47:51 -0400354 return schema
355
Brad Bishop2503bd62015-12-16 17:56:12 -0500356
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500357class InstanceHandler(RouteHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400358 verbs = ['GET', 'PUT', 'DELETE']
359 rules = '<path:path>'
360 request_type = dict
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500361
Brad Bishop87b63c12016-03-18 14:47:51 -0400362 def __init__(self, app, bus):
363 super(InstanceHandler, self).__init__(
364 app, bus, self.verbs, self.rules)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500365
Brad Bishop87b63c12016-03-18 14:47:51 -0400366 def find(self, path, callback=None):
367 return {path: self.try_mapper_call(
368 self.mapper.get_object,
369 callback,
370 path=path)}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500371
Brad Bishop87b63c12016-03-18 14:47:51 -0400372 def setup(self, path):
373 callback = None
374 if request.method == 'PUT':
375 def callback(e, **kw):
376 abort(403, _4034_msg % ('resource', 'created', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500377
Brad Bishop87b63c12016-03-18 14:47:51 -0400378 if request.route_data.get('map') is None:
379 request.route_data['map'] = self.find(path, callback)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500380
Brad Bishop87b63c12016-03-18 14:47:51 -0400381 def do_get(self, path):
Brad Bishop71527b42016-04-01 14:51:14 -0400382 return self.mapper.enumerate_object(
383 path,
384 mapper_data=request.route_data['map'])
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500385
Brad Bishop87b63c12016-03-18 14:47:51 -0400386 def do_put(self, path):
387 # make sure all properties exist in the request
388 obj = set(self.do_get(path).keys())
389 req = set(request.parameter_list.keys())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500390
Brad Bishop87b63c12016-03-18 14:47:51 -0400391 diff = list(obj.difference(req))
392 if diff:
393 abort(403, _4034_msg % (
394 'resource', 'removed', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500395
Brad Bishop87b63c12016-03-18 14:47:51 -0400396 diff = list(req.difference(obj))
397 if diff:
398 abort(403, _4034_msg % (
399 'resource', 'created', '%s/attr/%s' % (path, diff[0])))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500400
Brad Bishop87b63c12016-03-18 14:47:51 -0400401 for p, v in request.parameter_list.iteritems():
402 self.app.property_handler.do_put(
403 path, p, v)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500404
Brad Bishop87b63c12016-03-18 14:47:51 -0400405 def do_delete(self, path):
406 for bus_info in request.route_data['map'][path].iteritems():
407 if self.bus_missing_delete(path, *bus_info):
408 abort(403, _4034_msg % ('resource', 'removed', path))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500409
Brad Bishop87b63c12016-03-18 14:47:51 -0400410 for bus in request.route_data['map'][path].iterkeys():
411 self.delete_on_bus(path, bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500412
Brad Bishop87b63c12016-03-18 14:47:51 -0400413 def bus_missing_delete(self, path, bus, interfaces):
414 return DELETE_IFACE not in interfaces
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500415
Brad Bishop87b63c12016-03-18 14:47:51 -0400416 def delete_on_bus(self, path, bus):
417 obj = self.bus.get_object(bus, path, introspect=False)
418 delete_iface = dbus.Interface(
419 obj, dbus_interface=DELETE_IFACE)
420 delete_iface.Delete()
421
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500422
Brad Bishop2f428582015-12-02 10:56:11 -0500423class SessionHandler(MethodHandler):
Brad Bishop87b63c12016-03-18 14:47:51 -0400424 ''' Handles the /login and /logout routes, manages
425 server side session store and session cookies. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500426
Brad Bishop87b63c12016-03-18 14:47:51 -0400427 rules = ['/login', '/logout']
428 login_str = "User '%s' logged %s"
429 bad_passwd_str = "Invalid username or password"
430 no_user_str = "No user logged in"
431 bad_json_str = "Expecting request format { 'data': " \
432 "[<username>, <password>] }, got '%s'"
433 _require_auth = None
434 MAX_SESSIONS = 16
Brad Bishop2f428582015-12-02 10:56:11 -0500435
Brad Bishop87b63c12016-03-18 14:47:51 -0400436 def __init__(self, app, bus):
437 super(SessionHandler, self).__init__(
438 app, bus)
439 self.hmac_key = os.urandom(128)
440 self.session_store = []
Brad Bishop2f428582015-12-02 10:56:11 -0500441
Brad Bishop87b63c12016-03-18 14:47:51 -0400442 @staticmethod
443 def authenticate(username, clear):
444 try:
445 encoded = spwd.getspnam(username)[1]
446 return encoded == crypt.crypt(clear, encoded)
447 except KeyError:
448 return False
Brad Bishop2f428582015-12-02 10:56:11 -0500449
Brad Bishop87b63c12016-03-18 14:47:51 -0400450 def invalidate_session(self, session):
451 try:
452 self.session_store.remove(session)
453 except ValueError:
454 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500455
Brad Bishop87b63c12016-03-18 14:47:51 -0400456 def new_session(self):
457 sid = os.urandom(32)
458 if self.MAX_SESSIONS <= len(self.session_store):
459 self.session_store.pop()
460 self.session_store.insert(0, {'sid': sid})
Brad Bishop2f428582015-12-02 10:56:11 -0500461
Brad Bishop87b63c12016-03-18 14:47:51 -0400462 return self.session_store[0]
Brad Bishop2f428582015-12-02 10:56:11 -0500463
Brad Bishop87b63c12016-03-18 14:47:51 -0400464 def get_session(self, sid):
465 sids = [x['sid'] for x in self.session_store]
466 try:
467 return self.session_store[sids.index(sid)]
468 except ValueError:
469 return None
Brad Bishop2f428582015-12-02 10:56:11 -0500470
Brad Bishop87b63c12016-03-18 14:47:51 -0400471 def get_session_from_cookie(self):
472 return self.get_session(
473 request.get_cookie(
474 'sid', secret=self.hmac_key))
Brad Bishop2f428582015-12-02 10:56:11 -0500475
Brad Bishop87b63c12016-03-18 14:47:51 -0400476 def do_post(self, **kw):
477 if request.path == '/login':
478 return self.do_login(**kw)
479 else:
480 return self.do_logout(**kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500481
Brad Bishop87b63c12016-03-18 14:47:51 -0400482 def do_logout(self, **kw):
483 session = self.get_session_from_cookie()
484 if session is not None:
485 user = session['user']
486 self.invalidate_session(session)
487 response.delete_cookie('sid')
488 return self.login_str % (user, 'out')
Brad Bishop2f428582015-12-02 10:56:11 -0500489
Brad Bishop87b63c12016-03-18 14:47:51 -0400490 return self.no_user_str
Brad Bishop2f428582015-12-02 10:56:11 -0500491
Brad Bishop87b63c12016-03-18 14:47:51 -0400492 def do_login(self, **kw):
493 session = self.get_session_from_cookie()
494 if session is not None:
495 return self.login_str % (session['user'], 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500496
Brad Bishop87b63c12016-03-18 14:47:51 -0400497 if len(request.parameter_list) != 2:
498 abort(400, self.bad_json_str % (request.json))
Brad Bishop2f428582015-12-02 10:56:11 -0500499
Brad Bishop87b63c12016-03-18 14:47:51 -0400500 if not self.authenticate(*request.parameter_list):
501 return self.bad_passwd_str
Brad Bishop2f428582015-12-02 10:56:11 -0500502
Brad Bishop87b63c12016-03-18 14:47:51 -0400503 user = request.parameter_list[0]
504 session = self.new_session()
505 session['user'] = user
506 response.set_cookie(
507 'sid', session['sid'], secret=self.hmac_key,
508 secure=True,
509 httponly=True)
510 return self.login_str % (user, 'in')
Brad Bishop2f428582015-12-02 10:56:11 -0500511
Brad Bishop87b63c12016-03-18 14:47:51 -0400512 def find(self, **kw):
513 pass
Brad Bishop2f428582015-12-02 10:56:11 -0500514
Brad Bishop87b63c12016-03-18 14:47:51 -0400515 def setup(self, **kw):
516 pass
517
Brad Bishop2f428582015-12-02 10:56:11 -0500518
519class AuthorizationPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400520 ''' Invokes an optional list of authorization callbacks. '''
Brad Bishop2f428582015-12-02 10:56:11 -0500521
Brad Bishop87b63c12016-03-18 14:47:51 -0400522 name = 'authorization'
523 api = 2
Brad Bishop2f428582015-12-02 10:56:11 -0500524
Brad Bishop87b63c12016-03-18 14:47:51 -0400525 class Compose:
526 def __init__(self, validators, callback, session_mgr):
527 self.validators = validators
528 self.callback = callback
529 self.session_mgr = session_mgr
Brad Bishop2f428582015-12-02 10:56:11 -0500530
Brad Bishop87b63c12016-03-18 14:47:51 -0400531 def __call__(self, *a, **kw):
532 sid = request.get_cookie('sid', secret=self.session_mgr.hmac_key)
533 session = self.session_mgr.get_session(sid)
534 for x in self.validators:
535 x(session, *a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500536
Brad Bishop87b63c12016-03-18 14:47:51 -0400537 return self.callback(*a, **kw)
Brad Bishop2f428582015-12-02 10:56:11 -0500538
Brad Bishop87b63c12016-03-18 14:47:51 -0400539 def apply(self, callback, route):
540 undecorated = route.get_undecorated_callback()
541 if not isinstance(undecorated, RouteHandler):
542 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500543
Brad Bishop87b63c12016-03-18 14:47:51 -0400544 auth_types = getattr(
545 undecorated, '_require_auth', None)
546 if not auth_types:
547 return callback
Brad Bishop2f428582015-12-02 10:56:11 -0500548
Brad Bishop87b63c12016-03-18 14:47:51 -0400549 return self.Compose(
550 auth_types, callback, undecorated.app.session_handler)
551
Brad Bishop2f428582015-12-02 10:56:11 -0500552
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500553class JsonApiRequestPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400554 ''' Ensures request content satisfies the OpenBMC json api format. '''
555 name = 'json_api_request'
556 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500557
Brad Bishop87b63c12016-03-18 14:47:51 -0400558 error_str = "Expecting request format { 'data': <value> }, got '%s'"
559 type_error_str = "Unsupported Content-Type: '%s'"
560 json_type = "application/json"
561 request_methods = ['PUT', 'POST', 'PATCH']
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500562
Brad Bishop87b63c12016-03-18 14:47:51 -0400563 @staticmethod
564 def content_expected():
565 return request.method in JsonApiRequestPlugin.request_methods
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500566
Brad Bishop87b63c12016-03-18 14:47:51 -0400567 def validate_request(self):
568 if request.content_length > 0 and \
569 request.content_type != self.json_type:
570 abort(415, self.type_error_str % request.content_type)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500571
Brad Bishop87b63c12016-03-18 14:47:51 -0400572 try:
573 request.parameter_list = request.json.get('data')
574 except ValueError, e:
575 abort(400, str(e))
576 except (AttributeError, KeyError, TypeError):
577 abort(400, self.error_str % request.json)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500578
Brad Bishop87b63c12016-03-18 14:47:51 -0400579 def apply(self, callback, route):
580 verbs = getattr(
581 route.get_undecorated_callback(), '_verbs', None)
582 if verbs is None:
583 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500584
Brad Bishop87b63c12016-03-18 14:47:51 -0400585 if not set(self.request_methods).intersection(verbs):
586 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500587
Brad Bishop87b63c12016-03-18 14:47:51 -0400588 def wrap(*a, **kw):
589 if self.content_expected():
590 self.validate_request()
591 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500592
Brad Bishop87b63c12016-03-18 14:47:51 -0400593 return wrap
594
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500595
596class JsonApiRequestTypePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400597 ''' Ensures request content type satisfies the OpenBMC json api format. '''
598 name = 'json_api_method_request'
599 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500600
Brad Bishop87b63c12016-03-18 14:47:51 -0400601 error_str = "Expecting request format { 'data': %s }, got '%s'"
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500602
Brad Bishop87b63c12016-03-18 14:47:51 -0400603 def apply(self, callback, route):
604 request_type = getattr(
605 route.get_undecorated_callback(), 'request_type', None)
606 if request_type is None:
607 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500608
Brad Bishop87b63c12016-03-18 14:47:51 -0400609 def validate_request():
610 if not isinstance(request.parameter_list, request_type):
611 abort(400, self.error_str % (str(request_type), request.json))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500612
Brad Bishop87b63c12016-03-18 14:47:51 -0400613 def wrap(*a, **kw):
614 if JsonApiRequestPlugin.content_expected():
615 validate_request()
616 return callback(*a, **kw)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500617
Brad Bishop87b63c12016-03-18 14:47:51 -0400618 return wrap
619
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500620
621class JsonApiResponsePlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400622 ''' Emits normal responses in the OpenBMC json api format. '''
623 name = 'json_api_response'
624 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500625
Brad Bishop87b63c12016-03-18 14:47:51 -0400626 def apply(self, callback, route):
627 def wrap(*a, **kw):
628 resp = {'data': callback(*a, **kw)}
629 resp['status'] = 'ok'
630 resp['message'] = response.status_line
631 return resp
632 return wrap
633
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500634
635class JsonApiErrorsPlugin(object):
Brad Bishop87b63c12016-03-18 14:47:51 -0400636 ''' Emits error responses in the OpenBMC json api format. '''
637 name = 'json_api_errors'
638 api = 2
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500639
Brad Bishop87b63c12016-03-18 14:47:51 -0400640 def __init__(self, **kw):
641 self.app = None
642 self.function_type = None
643 self.original = None
644 self.json_opts = {
645 x: y for x, y in kw.iteritems()
646 if x in ['indent', 'sort_keys']}
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500647
Brad Bishop87b63c12016-03-18 14:47:51 -0400648 def setup(self, app):
649 self.app = app
650 self.function_type = type(app.default_error_handler)
651 self.original = app.default_error_handler
652 self.app.default_error_handler = self.function_type(
653 self.json_errors, app, Bottle)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500654
Brad Bishop87b63c12016-03-18 14:47:51 -0400655 def apply(self, callback, route):
656 return callback
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500657
Brad Bishop87b63c12016-03-18 14:47:51 -0400658 def close(self):
659 self.app.default_error_handler = self.function_type(
660 self.original, self.app, Bottle)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500661
Brad Bishop87b63c12016-03-18 14:47:51 -0400662 def json_errors(self, res, error):
663 response_object = {'status': 'error', 'data': {}}
664 response_object['message'] = error.status_line
665 response_object['data']['description'] = str(error.body)
666 if error.status_code == 500:
667 response_object['data']['exception'] = repr(error.exception)
668 response_object['data']['traceback'] = error.traceback.splitlines()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500669
Brad Bishop87b63c12016-03-18 14:47:51 -0400670 json_response = json.dumps(response_object, **self.json_opts)
671 response.content_type = 'application/json'
672 return json_response
673
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500674
Brad Bishop80fe37a2016-03-29 10:54:54 -0400675class JsonpPlugin(JsonApiErrorsPlugin):
676 ''' Json javascript wrapper. '''
677 name = 'jsonp'
678 api = 2
679
680 def __init__(self, **kw):
681 super(JsonpPlugin, self).__init__(**kw)
682
683 @staticmethod
684 def to_jsonp(json):
685 jwrapper = request.query.callback or None
686 if(jwrapper):
687 response.set_header('Content-Type', 'application/javascript')
688 json = jwrapper + '(' + json + ');'
689 return json
690
691 def apply(self, callback, route):
692 def wrap(*a, **kw):
693 return self.to_jsonp(callback(*a, **kw))
694 return wrap
695
696 def json_errors(self, res, error):
697 json = super(JsonpPlugin, self).json_errors(res, error)
698 return self.to_jsonp(json)
699
700
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500701class RestApp(Bottle):
Brad Bishop87b63c12016-03-18 14:47:51 -0400702 def __init__(self, bus):
703 super(RestApp, self).__init__(autojson=False)
704 self.bus = bus
Brad Bishopb103d2d2016-03-04 16:19:14 -0500705 self.mapper = obmc.mapper.Mapper(bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500706
Brad Bishop87b63c12016-03-18 14:47:51 -0400707 self.install_hooks()
708 self.install_plugins()
709 self.create_handlers()
710 self.install_handlers()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500711
Brad Bishop87b63c12016-03-18 14:47:51 -0400712 def install_plugins(self):
713 # install json api plugins
714 json_kw = {'indent': 2, 'sort_keys': True}
Brad Bishop87b63c12016-03-18 14:47:51 -0400715 self.install(AuthorizationPlugin())
Brad Bishop80fe37a2016-03-29 10:54:54 -0400716 self.install(JsonpPlugin(**json_kw))
717 self.install(JSONPlugin(**json_kw))
Brad Bishop87b63c12016-03-18 14:47:51 -0400718 self.install(JsonApiResponsePlugin())
719 self.install(JsonApiRequestPlugin())
720 self.install(JsonApiRequestTypePlugin())
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500721
Brad Bishop87b63c12016-03-18 14:47:51 -0400722 def install_hooks(self):
723 self.real_router_match = self.router.match
724 self.router.match = self.custom_router_match
725 self.add_hook('before_request', self.strip_extra_slashes)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500726
Brad Bishop87b63c12016-03-18 14:47:51 -0400727 def create_handlers(self):
728 # create route handlers
729 self.session_handler = SessionHandler(self, self.bus)
730 self.directory_handler = DirectoryHandler(self, self.bus)
731 self.list_names_handler = ListNamesHandler(self, self.bus)
732 self.list_handler = ListHandler(self, self.bus)
733 self.method_handler = MethodHandler(self, self.bus)
734 self.property_handler = PropertyHandler(self, self.bus)
735 self.schema_handler = SchemaHandler(self, self.bus)
736 self.instance_handler = InstanceHandler(self, self.bus)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500737
Brad Bishop87b63c12016-03-18 14:47:51 -0400738 def install_handlers(self):
739 self.session_handler.install()
740 self.directory_handler.install()
741 self.list_names_handler.install()
742 self.list_handler.install()
743 self.method_handler.install()
744 self.property_handler.install()
745 self.schema_handler.install()
746 # this has to come last, since it matches everything
747 self.instance_handler.install()
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500748
Brad Bishop87b63c12016-03-18 14:47:51 -0400749 def custom_router_match(self, environ):
750 ''' The built-in Bottle algorithm for figuring out if a 404 or 405 is
751 needed doesn't work for us since the instance rules match
752 everything. This monkey-patch lets the route handler figure
753 out which response is needed. This could be accomplished
754 with a hook but that would require calling the router match
755 function twice.
756 '''
757 route, args = self.real_router_match(environ)
758 if isinstance(route.callback, RouteHandler):
759 route.callback._setup(**args)
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500760
Brad Bishop87b63c12016-03-18 14:47:51 -0400761 return route, args
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500762
Brad Bishop87b63c12016-03-18 14:47:51 -0400763 @staticmethod
764 def strip_extra_slashes():
765 path = request.environ['PATH_INFO']
766 trailing = ("", "/")[path[-1] == '/']
767 parts = filter(bool, path.split('/'))
768 request.environ['PATH_INFO'] = '/' + '/'.join(parts) + trailing
Brad Bishopaa65f6e2015-10-27 16:28:51 -0400769
770if __name__ == '__main__':
Brad Bishop87b63c12016-03-18 14:47:51 -0400771 log = logging.getLogger('Rocket.Errors')
772 log.setLevel(logging.INFO)
773 log.addHandler(logging.StreamHandler(sys.stdout))
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500774
Brad Bishop87b63c12016-03-18 14:47:51 -0400775 bus = dbus.SystemBus()
776 app = RestApp(bus)
777 default_cert = os.path.join(
778 sys.prefix, 'share', os.path.basename(__file__), 'cert.pem')
Brad Bishopb1cbdaf2015-11-13 21:28:16 -0500779
Brad Bishop87b63c12016-03-18 14:47:51 -0400780 server = Rocket(
781 ('0.0.0.0', 443, default_cert, default_cert),
782 'wsgi', {'wsgi_app': app},
783 min_threads=1,
784 max_threads=1)
785 server.start()