Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

414

415

416

417

418

419

420

421

422

423

424

425

426

427

428

429

430

431

432

433

434

435

436

437

438

439

440

441

442

443

444

445

446

447

448

449

450

451

452

453

454

455

456

457

458

459

460

461

462

463

464

465

466

467

468

469

470

471

472

473

474

475

476

477

478

479

480

481

482

483

484

485

486

487

488

489

490

491

492

493

494

495

496

497

498

499

500

501

502

503

504

505

506

507

508

509

510

511

512

513

514

515

516

517

518

519

520

521

522

523

524

525

526

527

528

529

530

531

# vim: tabstop=4 shiftwidth=4 softtabstop=4 

 

# Copyright 2012 OpenStack LLC 

# Copyright 2010 United States Government as represented by the 

# Administrator of the National Aeronautics and Space Administration. 

# Copyright 2010 OpenStack LLC. 

# All Rights Reserved. 

# 

#    Licensed under the Apache License, Version 2.0 (the "License"); you may 

#    not use this file except in compliance with the License. You may obtain 

#    a copy of the License at 

# 

#         http://www.apache.org/licenses/LICENSE-2.0 

# 

#    Unless required by applicable law or agreed to in writing, software 

#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

#    License for the specific language governing permissions and limitations 

#    under the License. 

 

"""Utility methods for working with WSGI servers.""" 

 

import re 

 

import routes.middleware 

import webob.dec 

import webob.exc 

 

from keystone.common import config 

from keystone.common import logging 

from keystone.common import utils 

from keystone import exception 

from keystone.openstack.common import importutils 

from keystone.openstack.common import jsonutils 

 

 

CONF = config.CONF 

LOG = logging.getLogger(__name__) 

 

# Environment variable used to pass the request context 

CONTEXT_ENV = 'openstack.context' 

 

 

# Environment variable used to pass the request params 

PARAMS_ENV = 'openstack.params' 

 

 

_RE_PASS = re.compile(r'([\'"].*?password[\'"]\s*:\s*u?[\'"]).*?([\'"])', 

                      re.DOTALL) 

 

 

def mask_password(message, is_unicode=False, secret="***"): 

    """Replace password with 'secret' in message. 

 

    :param message: The string which include security information. 

    :param is_unicode: Is unicode string ? 

    :param secret: substitution string default to "***". 

    :returns: The string 

 

    For example: 

       >>> mask_password('"password" : "aaaaa"') 

       '"password" : "***"' 

       >>> mask_password("'original_password' : 'aaaaa'") 

       "'original_password' : '***'" 

       >>> mask_password("u'original_password' :   u'aaaaa'") 

       "u'original_password' :   u'***'" 

    """ 

    if is_unicode: 

        message = unicode(message) 

    # Match the group 1,2 and replace all others with 'secret' 

    secret = r"\g<1>" + secret + r"\g<2>" 

    result = _RE_PASS.sub(secret, message) 

    return result 

 

 

class WritableLogger(object): 

    """A thin wrapper that responds to `write` and logs.""" 

 

    def __init__(self, logger, level=logging.DEBUG): 

        self.logger = logger 

        self.level = level 

 

    def write(self, msg): 

        self.logger.log(self.level, msg) 

 

 

class Request(webob.Request): 

    pass 

 

 

class BaseApplication(object): 

    """Base WSGI application wrapper. Subclasses need to implement __call__.""" 

 

    @classmethod 

    def factory(cls, global_config, **local_config): 

        """Used for paste app factories in paste.deploy config files. 

 

        Any local configuration (that is, values under the [app:APPNAME] 

        section of the paste config) will be passed into the `__init__` method 

        as kwargs. 

 

        A hypothetical configuration would look like: 

 

            [app:wadl] 

            latest_version = 1.3 

            paste.app_factory = nova.api.fancy_api:Wadl.factory 

 

        which would result in a call to the `Wadl` class as 

 

            import nova.api.fancy_api 

            fancy_api.Wadl(latest_version='1.3') 

 

        You could of course re-implement the `factory` method in subclasses, 

        but using the kwarg passing it shouldn't be necessary. 

 

        """ 

        return cls() 

 

    def __call__(self, environ, start_response): 

        r"""Subclasses will probably want to implement __call__ like this: 

 

        @webob.dec.wsgify(RequestClass=Request) 

        def __call__(self, req): 

          # Any of the following objects work as responses: 

 

          # Option 1: simple string 

          res = 'message\n' 

 

          # Option 2: a nicely formatted HTTP exception page 

          res = exc.HTTPForbidden(detail='Nice try') 

 

          # Option 3: a webob Response object (in case you need to play with 

          # headers, or you want to be treated like an iterable, or or or) 

          res = Response(); 

          res.app_iter = open('somefile') 

 

          # Option 4: any wsgi app to be run next 

          res = self.application 

 

          # Option 5: you can get a Response object for a wsgi app, too, to 

          # play with headers etc 

          res = req.get_response(self.application) 

 

          # You can then just return your response... 

          return res 

          # ... or set req.response and return None. 

          req.response = res 

 

        See the end of http://pythonpaste.org/webob/modules/dec.html 

        for more info. 

 

        """ 

        raise NotImplementedError('You must implement __call__') 

 

 

class Application(BaseApplication): 

    @webob.dec.wsgify 

    def __call__(self, req): 

        arg_dict = req.environ['wsgiorg.routing_args'][1] 

        action = arg_dict.pop('action') 

        del arg_dict['controller'] 

        LOG.debug(_('arg_dict: %s'), arg_dict) 

 

        # allow middleware up the stack to provide context & params 

        context = req.environ.get(CONTEXT_ENV, {}) 

        context['query_string'] = dict(req.params.iteritems()) 

        context['path'] = req.environ['PATH_INFO'] 

        params = req.environ.get(PARAMS_ENV, {}) 

170        if 'REMOTE_USER' in req.environ: 

            context['REMOTE_USER'] = req.environ['REMOTE_USER'] 

172        elif context.get('REMOTE_USER', None) is not None: 

            del context['REMOTE_USER'] 

        params.update(arg_dict) 

 

        # TODO(termie): do some basic normalization on methods 

        method = getattr(self, action) 

 

        # NOTE(vish): make sure we have no unicode keys for py2.6. 

        params = self._normalize_dict(params) 

 

        try: 

            result = method(context, **params) 

        except exception.Unauthorized as e: 

            LOG.warning( 

                _('Authorization failed. %(exception)s from %(remote_addr)s') % 

                {'exception': e, 'remote_addr': req.environ['REMOTE_ADDR']}) 

            return render_exception(e) 

        except exception.Error as e: 

            LOG.warning(e) 

            return render_exception(e) 

        except TypeError as e: 

            LOG.exception(e) 

            return render_exception(exception.ValidationError(e)) 

        except Exception as e: 

            LOG.exception(e) 

            return render_exception(exception.UnexpectedError(exception=e)) 

 

        if result is None: 

            return render_response(status=(204, 'No Content')) 

201        elif isinstance(result, basestring): 

            return result 

        elif isinstance(result, webob.Response): 

            return result 

205        elif isinstance(result, webob.exc.WSGIHTTPException): 

            return result 

 

        response_code = self._get_response_code(req) 

        return render_response(body=result, status=response_code) 

 

    def _get_response_code(self, req): 

        req_method = req.environ['REQUEST_METHOD'] 

        controller = importutils.import_class('keystone.common.controller') 

        code = None 

        if isinstance(self, controller.V3Controller) and req_method == 'POST': 

            code = (201, 'Created') 

        return code 

 

    def _normalize_arg(self, arg): 

        return str(arg).replace(':', '_').replace('-', '_') 

 

    def _normalize_dict(self, d): 

        return dict([(self._normalize_arg(k), v) 

                     for (k, v) in d.iteritems()]) 

 

    def assert_admin(self, context): 

        if not context['is_admin']: 

            try: 

                user_token_ref = self.token_api.get_token( 

                    context=context, token_id=context['token_id']) 

            except exception.TokenNotFound as e: 

                raise exception.Unauthorized(e) 

 

            creds = user_token_ref['metadata'].copy() 

 

            try: 

                creds['user_id'] = user_token_ref['user'].get('id') 

            except AttributeError: 

                LOG.debug('Invalid user') 

                raise exception.Unauthorized() 

 

            try: 

                creds['tenant_id'] = user_token_ref['tenant'].get('id') 

            except AttributeError: 

                LOG.debug('Invalid tenant') 

                raise exception.Unauthorized() 

 

            # NOTE(vish): this is pretty inefficient 

            creds['roles'] = [self.identity_api.get_role(context, role)['name'] 

                              for role in creds.get('roles', [])] 

            # Accept either is_admin or the admin role 

            self.policy_api.enforce(context, creds, 'admin_required', {}) 

 

 

class Middleware(Application): 

    """Base WSGI middleware. 

 

    These classes require an application to be 

    initialized that will be called next.  By default the middleware will 

    simply call its wrapped app, or you can override __call__ to customize its 

    behavior. 

 

    """ 

 

    @classmethod 

    def factory(cls, global_config, **local_config): 

        """Used for paste app factories in paste.deploy config files. 

 

        Any local configuration (that is, values under the [filter:APPNAME] 

        section of the paste config) will be passed into the `__init__` method 

        as kwargs. 

 

        A hypothetical configuration would look like: 

 

            [filter:analytics] 

            redis_host = 127.0.0.1 

            paste.filter_factory = nova.api.analytics:Analytics.factory 

 

        which would result in a call to the `Analytics` class as 

 

            import nova.api.analytics 

            analytics.Analytics(app_from_paste, redis_host='127.0.0.1') 

 

        You could of course re-implement the `factory` method in subclasses, 

        but using the kwarg passing it shouldn't be necessary. 

 

        """ 

        def _factory(app): 

            conf = global_config.copy() 

            conf.update(local_config) 

            return cls(app) 

        return _factory 

 

    def __init__(self, application): 

        self.application = application 

 

    def process_request(self, request): 

        """Called on each request. 

 

        If this returns None, the next application down the stack will be 

        executed. If it returns a response then that response will be returned 

        and execution will stop here. 

 

        """ 

        return None 

 

    def process_response(self, request, response): 

        """Do whatever you'd like to the response, based on the request.""" 

        return response 

 

    @webob.dec.wsgify(RequestClass=Request) 

    def __call__(self, request): 

        try: 

            response = self.process_request(request) 

            if response: 

                return response 

            response = request.get_response(self.application) 

            return self.process_response(request, response) 

        except exception.Error as e: 

            LOG.warning(e) 

            return render_exception(e) 

        except TypeError as e: 

            LOG.exception(e) 

            return render_exception(exception.ValidationError(e)) 

        except Exception as e: 

            LOG.exception(e) 

            return render_exception(exception.UnexpectedError(exception=e)) 

 

 

class Debug(Middleware): 

    """Helper class for debugging a WSGI application. 

 

    Can be inserted into any WSGI application chain to get information 

    about the request and response. 

 

    """ 

 

    @webob.dec.wsgify(RequestClass=Request) 

    def __call__(self, req): 

        if LOG.isEnabledFor(logging.DEBUG): 

            LOG.debug('%s %s %s', ('*' * 20), 'REQUEST ENVIRON', ('*' * 20)) 

            for key, value in req.environ.items(): 

                LOG.debug('%s = %s', key, mask_password(value, 

                                                        is_unicode=True)) 

            LOG.debug('') 

            LOG.debug('%s %s %s', ('*' * 20), 'REQUEST BODY', ('*' * 20)) 

            for line in req.body_file: 

                LOG.debug(mask_password(line)) 

            LOG.debug('') 

 

        resp = req.get_response(self.application) 

        if LOG.isEnabledFor(logging.DEBUG): 

            LOG.debug('%s %s %s', ('*' * 20), 'RESPONSE HEADERS', ('*' * 20)) 

            for (key, value) in resp.headers.iteritems(): 

                LOG.debug('%s = %s', key, value) 

            LOG.debug('') 

 

        resp.app_iter = self.print_generator(resp.app_iter) 

 

        return resp 

 

    @staticmethod 

    def print_generator(app_iter): 

        """Iterator that prints the contents of a wrapper string.""" 

        LOG.debug('%s %s %s', ('*' * 20), 'RESPONSE BODY', ('*' * 20)) 

        for part in app_iter: 

            LOG.debug(part) 

            yield part 

 

 

class Router(object): 

    """WSGI middleware that maps incoming requests to WSGI apps.""" 

 

    def __init__(self, mapper): 

        """Create a router for the given routes.Mapper. 

 

        Each route in `mapper` must specify a 'controller', which is a 

        WSGI app to call.  You'll probably want to specify an 'action' as 

        well and have your controller be an object that can route 

        the request to the action-specific method. 

 

        Examples: 

          mapper = routes.Mapper() 

          sc = ServerController() 

 

          # Explicit mapping of one route to a controller+action 

          mapper.connect(None, '/svrlist', controller=sc, action='list') 

 

          # Actions are all implicitly defined 

          mapper.resource('server', 'servers', controller=sc) 

 

          # Pointing to an arbitrary WSGI app.  You can specify the 

          # {path_info:.*} parameter so the target app can be handed just that 

          # section of the URL. 

          mapper.connect(None, '/v1.0/{path_info:.*}', controller=BlogApp()) 

 

        """ 

        # if we're only running in debug, bump routes' internal logging up a 

        # notch, as it's very spammy 

400        if CONF.debug: 

            logging.getLogger('routes.middleware').setLevel(logging.INFO) 

 

        self.map = mapper 

        self._router = routes.middleware.RoutesMiddleware(self._dispatch, 

                                                          self.map) 

 

    @webob.dec.wsgify(RequestClass=Request) 

    def __call__(self, req): 

        """Route the incoming request to a controller based on self.map. 

 

        If no match, return a 404. 

 

        """ 

        return self._router 

 

    @staticmethod 

    @webob.dec.wsgify(RequestClass=Request) 

    def _dispatch(req): 

        """Dispatch the request to the appropriate controller. 

 

        Called by self._router after matching the incoming request to a route 

        and putting the information into req.environ.  Either returns 404 

        or the routed WSGI app's response. 

 

        """ 

        match = req.environ['wsgiorg.routing_args'][1] 

        if not match: 

            return render_exception( 

                exception.NotFound(_('The resource could not be found.'))) 

        app = match['controller'] 

        return app 

 

 

class ComposingRouter(Router): 

    def __init__(self, mapper=None, routers=None): 

436        if mapper is None: 

            mapper = routes.Mapper() 

438        if routers is None: 

            routers = [] 

        for router in routers: 

            router.add_routes(mapper) 

        super(ComposingRouter, self).__init__(mapper) 

 

 

class ComposableRouter(Router): 

    """Router that supports use by ComposingRouter.""" 

 

    def __init__(self, mapper=None): 

450        if mapper is None: 

            mapper = routes.Mapper() 

        self.add_routes(mapper) 

        super(ComposableRouter, self).__init__(mapper) 

 

    def add_routes(self, mapper): 

        """Add routes to given mapper.""" 

        pass 

 

 

class ExtensionRouter(Router): 

    """A router that allows extensions to supplement or overwrite routes. 

 

    Expects to be subclassed. 

    """ 

    def __init__(self, application, mapper=None): 

466        if mapper is None: 

            mapper = routes.Mapper() 

        self.application = application 

        self.add_routes(mapper) 

        mapper.connect('{path_info:.*}', controller=self.application) 

        super(ExtensionRouter, self).__init__(mapper) 

 

    def add_routes(self, mapper): 

        pass 

 

    @classmethod 

    def factory(cls, global_config, **local_config): 

        """Used for paste app factories in paste.deploy config files. 

 

        Any local configuration (that is, values under the [filter:APPNAME] 

        section of the paste config) will be passed into the `__init__` method 

        as kwargs. 

 

        A hypothetical configuration would look like: 

 

            [filter:analytics] 

            redis_host = 127.0.0.1 

            paste.filter_factory = nova.api.analytics:Analytics.factory 

 

        which would result in a call to the `Analytics` class as 

 

            import nova.api.analytics 

            analytics.Analytics(app_from_paste, redis_host='127.0.0.1') 

 

        You could of course re-implement the `factory` method in subclasses, 

        but using the kwarg passing it shouldn't be necessary. 

 

        """ 

        def _factory(app): 

            conf = global_config.copy() 

            conf.update(local_config) 

            return cls(app) 

        return _factory 

 

 

def render_response(body=None, status=None, headers=None): 

    """Forms a WSGI response.""" 

    headers = headers or [] 

    headers.append(('Vary', 'X-Auth-Token')) 

 

    if body is None: 

        body = '' 

        status = status or (204, 'No Content') 

    else: 

        body = jsonutils.dumps(body, cls=utils.SmarterEncoder) 

        headers.append(('Content-Type', 'application/json')) 

        status = status or (200, 'OK') 

 

    return webob.Response(body=body, 

                          status='%s %s' % status, 

                          headerlist=headers) 

 

 

def render_exception(error): 

    """Forms a WSGI response based on the current error.""" 

    body = {'error': { 

        'code': error.code, 

        'title': error.title, 

        'message': str(error) 

    }} 

530    if isinstance(error, exception.AuthPluginException): 

        body['error']['identity'] = error.authentication 

    return render_response(status=(error.code, error.title), body=body)