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

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

 

# Copyright 2012 OpenStack LLC 

# 

# 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. 

 

"""Main entry point into the EC2 Credentials service. 

 

This service allows the creation of access/secret credentials used for 

the ec2 interop layer of OpenStack. 

 

A user can create as many access/secret pairs, each of which map to a 

specific tenant.  This is required because OpenStack supports a user 

belonging to multiple tenants, whereas the signatures created on ec2-style 

requests don't allow specification of which tenant the user wishs to act 

upon. 

 

To complete the cycle, we provide a method that OpenStack services can 

use to validate a signature and get a corresponding openstack token.  This 

token allows method calls to other services within the context the 

access/secret was created.  As an example, nova requests keystone to validate 

the signature of a request, receives a token, and then makes a request to 

glance to list images needed to perform the requested task. 

 

""" 

 

import uuid 

 

from keystoneclient.contrib.ec2 import utils as ec2_utils 

 

from keystone.common import controller 

from keystone.common import dependency 

from keystone.common import manager 

from keystone.common import utils 

from keystone.common import wsgi 

from keystone import config 

from keystone import exception 

from keystone import token 

 

 

CONF = config.CONF 

 

 

@dependency.provider('ec2_api') 

class Manager(manager.Manager): 

    """Default pivot point for the EC2 Credentials backend. 

 

    See :mod:`keystone.common.manager.Manager` for more details on how this 

    dynamically calls the backend. 

 

    """ 

 

    def __init__(self): 

        super(Manager, self).__init__(CONF.ec2.driver) 

 

 

class Ec2Extension(wsgi.ExtensionRouter): 

    def add_routes(self, mapper): 

        ec2_controller = Ec2Controller() 

        # validation 

        mapper.connect( 

            '/ec2tokens', 

            controller=ec2_controller, 

            action='authenticate', 

            conditions=dict(method=['POST'])) 

 

        # crud 

        mapper.connect( 

            '/users/{user_id}/credentials/OS-EC2', 

            controller=ec2_controller, 

            action='create_credential', 

            conditions=dict(method=['POST'])) 

        mapper.connect( 

            '/users/{user_id}/credentials/OS-EC2', 

            controller=ec2_controller, 

            action='get_credentials', 

            conditions=dict(method=['GET'])) 

        mapper.connect( 

            '/users/{user_id}/credentials/OS-EC2/{credential_id}', 

            controller=ec2_controller, 

            action='get_credential', 

            conditions=dict(method=['GET'])) 

        mapper.connect( 

            '/users/{user_id}/credentials/OS-EC2/{credential_id}', 

            controller=ec2_controller, 

            action='delete_credential', 

            conditions=dict(method=['DELETE'])) 

 

 

@dependency.requires('catalog_api', 'ec2_api') 

class Ec2Controller(controller.V2Controller): 

    def check_signature(self, creds_ref, credentials): 

        signer = ec2_utils.Ec2Signer(creds_ref['secret']) 

        signature = signer.generate(credentials) 

        if utils.auth_str_equal(credentials['signature'], signature): 

            return 

        # NOTE(vish): Some libraries don't use the port when signing 

        #             requests, so try again without port. 

        elif ':' in credentials['signature']: 

            hostname, _port = credentials['host'].split(':') 

            credentials['host'] = hostname 

            signature = signer.generate(credentials) 

            if not utils.auth_str_equal(credentials.signature, signature): 

                raise exception.Unauthorized(message='Invalid EC2 signature.') 

        else: 

            raise exception.Unauthorized(message='EC2 signature not supplied.') 

 

    def authenticate(self, context, credentials=None, ec2Credentials=None): 

        """Validate a signed EC2 request and provide a token. 

 

        Other services (such as Nova) use this **admin** call to determine 

        if a request they signed received is from a valid user. 

 

        If it is a valid signature, an openstack token that maps 

        to the user/tenant is returned to the caller, along with 

        all the other details returned from a normal token validation 

        call. 

 

        The returned token is useful for making calls to other 

        OpenStack services within the context of the request. 

 

        :param context: standard context 

        :param credentials: dict of ec2 signature 

        :param ec2Credentials: DEPRECATED dict of ec2 signature 

        :returns: token: openstack token equivalent to access key along 

                         with the corresponding service catalog and roles 

        """ 

 

        # FIXME(ja): validate that a service token was used! 

 

        # NOTE(termie): backwards compat hack 

        if not credentials and ec2Credentials: 

            credentials = ec2Credentials 

 

        if 'access' not in credentials: 

            raise exception.Unauthorized(message='EC2 signature not supplied.') 

 

        creds_ref = self._get_credentials(context, 

                                          credentials['access']) 

        self.check_signature(creds_ref, credentials) 

 

        # TODO(termie): don't create new tokens every time 

        # TODO(termie): this is copied from TokenController.authenticate 

        token_id = uuid.uuid4().hex 

        tenant_ref = self.identity_api.get_project( 

            context=context, 

            tenant_id=creds_ref['tenant_id']) 

        user_ref = self.identity_api.get_user( 

            context=context, 

            user_id=creds_ref['user_id']) 

        metadata_ref = self.identity_api.get_metadata( 

            context=context, 

            user_id=user_ref['id'], 

            tenant_id=tenant_ref['id']) 

 

        # Validate that the auth info is valid and nothing is disabled 

        token.validate_auth_info(self, context, user_ref, tenant_ref) 

 

        # TODO(termie): optimize this call at some point and put it into the 

        #               the return for metadata 

        # fill out the roles in the metadata 

        roles = metadata_ref.get('roles', []) 

        if not roles: 

            raise exception.Unauthorized(message='User not valid for tenant.') 

        roles_ref = [self.identity_api.get_role(context, role_id) 

                     for role_id in roles] 

 

        catalog_ref = self.catalog_api.get_catalog( 

            context=context, 

            user_id=user_ref['id'], 

            tenant_id=tenant_ref['id'], 

            metadata=metadata_ref) 

 

        token_ref = self.token_api.create_token( 

            context, token_id, dict(id=token_id, 

                                    user=user_ref, 

                                    tenant=tenant_ref, 

                                    metadata=metadata_ref)) 

 

        # TODO(termie): i don't think the ec2 middleware currently expects a 

        #               full return, but it contains a note saying that it 

        #               would be better to expect a full return 

        return token.controllers.Auth.format_authenticate( 

            token_ref, roles_ref, catalog_ref) 

 

    def create_credential(self, context, user_id, tenant_id): 

        """Create a secret/access pair for use with ec2 style auth. 

 

        Generates a new set of credentials that map the the user/tenant 

        pair. 

 

        :param context: standard context 

        :param user_id: id of user 

        :param tenant_id: id of tenant 

        :returns: credential: dict of ec2 credential 

        """ 

208        if not self._is_admin(context): 

            self._assert_identity(context, user_id) 

 

        self._assert_valid_user_id(context, user_id) 

        self._assert_valid_project_id(context, tenant_id) 

 

        cred_ref = {'user_id': user_id, 

                    'tenant_id': tenant_id, 

                    'access': uuid.uuid4().hex, 

                    'secret': uuid.uuid4().hex} 

        self.ec2_api.create_credential(context, cred_ref['access'], cred_ref) 

        return {'credential': cred_ref} 

 

    def get_credentials(self, context, user_id): 

        """List all credentials for a user. 

 

        :param context: standard context 

        :param user_id: id of user 

        :returns: credentials: list of ec2 credential dicts 

        """ 

        if not self._is_admin(context): 

            self._assert_identity(context, user_id) 

        self._assert_valid_user_id(context, user_id) 

        return {'credentials': self.ec2_api.list_credentials(context, user_id)} 

 

    def get_credential(self, context, user_id, credential_id): 

        """Retrieve a user's access/secret pair by the access key. 

 

        Grab the full access/secret pair for a given access key. 

 

        :param context: standard context 

        :param user_id: id of user 

        :param credential_id: access key for credentials 

        :returns: credential: dict of ec2 credential 

        """ 

        if not self._is_admin(context): 

            self._assert_identity(context, user_id) 

        self._assert_valid_user_id(context, user_id) 

        creds = self._get_credentials(context, credential_id) 

        return {'credential': creds} 

 

    def delete_credential(self, context, user_id, credential_id): 

        """Delete a user's access/secret pair. 

 

        Used to revoke a user's access/secret pair 

 

        :param context: standard context 

        :param user_id: id of user 

        :param credential_id: access key for credentials 

        :returns: bool: success 

        """ 

        if not self._is_admin(context): 

            self._assert_identity(context, user_id) 

            self._assert_owner(context, user_id, credential_id) 

 

        self._assert_valid_user_id(context, user_id) 

        self._get_credentials(context, credential_id) 

        return self.ec2_api.delete_credential(context, credential_id) 

 

    def _get_credentials(self, context, credential_id): 

        """Return credentials from an ID. 

 

        :param context: standard context 

        :param credential_id: id of credential 

        :raises exception.Unauthorized: when credential id is invalid 

        :returns: credential: dict of ec2 credential. 

        """ 

        creds = self.ec2_api.get_credential(context, 

                                            credential_id) 

277        if not creds: 

            raise exception.Unauthorized(message='EC2 access key not found.') 

        return creds 

 

    def _assert_identity(self, context, user_id): 

        """Check that the provided token belongs to the user. 

 

        :param context: standard context 

        :param user_id: id of user 

        :raises exception.Forbidden: when token is invalid 

 

        """ 

        try: 

            token_ref = self.token_api.get_token( 

                context=context, 

                token_id=context['token_id']) 

        except exception.TokenNotFound as e: 

            raise exception.Unauthorized(e) 

 

exit        if token_ref['user'].get('id') != user_id: 

            raise exception.Forbidden('Token belongs to another user') 

 

    def _is_admin(self, context): 

        """Wrap admin assertion error return statement. 

 

        :param context: standard context 

        :returns: bool: success 

 

        """ 

        try: 

            self.assert_admin(context) 

            return True 

        except exception.Forbidden: 

            return False 

 

    def _assert_owner(self, context, user_id, credential_id): 

        """Ensure the provided user owns the credential. 

 

        :param context: standard context 

        :param user_id: expected credential owner 

        :param credential_id: id of credential object 

        :raises exception.Forbidden: on failure 

 

        """ 

        cred_ref = self.ec2_api.get_credential(context, credential_id) 

        if not user_id == cred_ref['user_id']: 

            raise exception.Forbidden('Credential belongs to another user') 

 

    def _assert_valid_user_id(self, context, user_id): 

        """Ensure a valid user id. 

 

        :param context: standard context 

        :param user_id: expected credential owner 

        :raises exception.UserNotFound: on failure 

 

        """ 

        user_ref = self.identity_api.get_user( 

            context=context, 

            user_id=user_id) 

336        if not user_ref: 

            raise exception.UserNotFound(user_id=user_id) 

 

    def _assert_valid_project_id(self, context, tenant_id): 

        """Ensure a valid tenant id. 

 

        :param context: standard context 

        :param tenant_id: expected tenant 

        :raises exception.ProjectNotFound: on failure 

 

        """ 

        tenant_ref = self.identity_api.get_project( 

            context=context, 

            tenant_id=tenant_id) 

350        if not tenant_ref: 

            raise exception.ProjectNotFound(project_id=tenant_id)