~ubuntu-branches/ubuntu/saucy/heat/saucy-updates

« back to all changes in this revision

Viewing changes to heat/common/auth.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short, Chuck Short, Adam Gandelman
  • Date: 2013-09-08 21:51:19 UTC
  • mfrom: (1.1.4)
  • Revision ID: package-import@ubuntu.com-20130908215119-r939tu4aumqgdrkx
Tags: 2013.2~b3-0ubuntu1
[ Chuck Short ]
* New upstream release.
* debian/control: Add python-netaddr as build-dep.
* debian/heat-common.install: Remove heat-boto and associated man-page
* debian/heat-common.install: Remove heat-cfn and associated man-page
* debian/heat-common.install: Remove heat-watch and associated man-page
* debian/patches/fix-sqlalchemy-0.8.patch: Dropped

[ Adam Gandelman ]
* debian/patches/default-kombu.patch: Dropped.
* debian/patches/default-sqlite.patch: Refreshed.
* debian/*.install, rules: Install heat.conf.sample as common
  config file in heat-common. Drop other per-package configs, they
  are no longer used.
* debian/rules: Clean pbr .egg from build dir if it exists.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
 
 
3
 
#
4
 
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
5
 
#    not use this file except in compliance with the License. You may obtain
6
 
#    a copy of the License at
7
 
#
8
 
#         http://www.apache.org/licenses/LICENSE-2.0
9
 
#
10
 
#    Unless required by applicable law or agreed to in writing, software
11
 
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12
 
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13
 
#    License for the specific language governing permissions and limitations
14
 
#    under the License.
15
 
 
16
 
"""
17
 
This auth module is intended to allow Openstack client-tools to select from a
18
 
variety of authentication strategies, including NoAuth (the default), and
19
 
Keystone (an identity management system).
20
 
 
21
 
    > auth_plugin = AuthPlugin(creds)
22
 
 
23
 
    > auth_plugin.authenticate()
24
 
 
25
 
    > auth_plugin.auth_token
26
 
    abcdefg
27
 
 
28
 
    > auth_plugin.management_url
29
 
    http://service_endpoint/
30
 
"""
31
 
import httplib2
32
 
import json
33
 
import urlparse
34
 
 
35
 
from heat.common import exception
36
 
 
37
 
from heat.openstack.common.gettextutils import _
38
 
 
39
 
 
40
 
class BaseStrategy(object):
41
 
    def __init__(self):
42
 
        self.auth_token = None
43
 
        # TODO(sirp): Should expose selecting public/internal/admin URL.
44
 
        self.management_url = None
45
 
 
46
 
    def authenticate(self):
47
 
        raise NotImplementedError
48
 
 
49
 
    @property
50
 
    def is_authenticated(self):
51
 
        raise NotImplementedError
52
 
 
53
 
    @property
54
 
    def strategy(self):
55
 
        raise NotImplementedError
56
 
 
57
 
 
58
 
class NoAuthStrategy(BaseStrategy):
59
 
    def authenticate(self):
60
 
        pass
61
 
 
62
 
    @property
63
 
    def is_authenticated(self):
64
 
        return True
65
 
 
66
 
    @property
67
 
    def strategy(self):
68
 
        return 'noauth'
69
 
 
70
 
 
71
 
class KeystoneStrategy(BaseStrategy):
72
 
    MAX_REDIRECTS = 10
73
 
 
74
 
    def __init__(self, creds, service_type):
75
 
        self.creds = creds
76
 
        self.service_type = service_type
77
 
        super(KeystoneStrategy, self).__init__()
78
 
 
79
 
    def check_auth_params(self):
80
 
        # Ensure that supplied credential parameters are as required
81
 
        for required in ('username', 'password', 'auth_url',
82
 
                         'strategy'):
83
 
            if required not in self.creds:
84
 
                raise exception.MissingCredentialError(required=required)
85
 
        if self.creds['strategy'] != 'keystone':
86
 
            raise exception.BadAuthStrategy(expected='keystone',
87
 
                                            received=self.creds['strategy'])
88
 
        # For v2.0 also check tenant is present
89
 
        if self.creds['auth_url'].rstrip('/').endswith('v2.0'):
90
 
            if 'tenant' not in self.creds:
91
 
                raise exception.MissingCredentialError(required='tenant')
92
 
 
93
 
    def authenticate(self):
94
 
        """Authenticate with the Keystone service.
95
 
 
96
 
        There are a few scenarios to consider here:
97
 
 
98
 
        1. Which version of Keystone are we using? v1 which uses headers to
99
 
           pass the credentials, or v2 which uses a JSON encoded request body?
100
 
 
101
 
        2. Keystone may respond back with a redirection using a 305 status
102
 
           code.
103
 
 
104
 
        3. We may attempt a v1 auth when v2 is what's called for. In this
105
 
           case, we rewrite the url to contain /v2.0/ and retry using the v2
106
 
           protocol.
107
 
        """
108
 
        def _authenticate(auth_url):
109
 
            # If OS_AUTH_URL is missing a trailing slash add one
110
 
            if not auth_url.endswith('/'):
111
 
                auth_url += '/'
112
 
            token_url = urlparse.urljoin(auth_url, "tokens")
113
 
            # 1. Check Keystone version
114
 
            is_v2 = auth_url.rstrip('/').endswith('v2.0')
115
 
            if is_v2:
116
 
                self._v2_auth(token_url)
117
 
            else:
118
 
                self._v1_auth(token_url)
119
 
 
120
 
        self.check_auth_params()
121
 
        auth_url = self.creds['auth_url']
122
 
        for x in range(self.MAX_REDIRECTS):
123
 
            try:
124
 
                _authenticate(auth_url)
125
 
            except exception.AuthorizationRedirect as e:
126
 
                # 2. Keystone may redirect us
127
 
                auth_url = e.url
128
 
            except exception.AuthorizationFailure:
129
 
                # 3. In some configurations nova makes redirection to
130
 
                # v2.0 keystone endpoint. Also, new location does not
131
 
                # contain real endpoint, only hostname and port.
132
 
                if 'v2.0' not in auth_url:
133
 
                    auth_url = urlparse.urljoin(auth_url, 'v2.0/')
134
 
            else:
135
 
                # If we sucessfully auth'd, then memorize the correct auth_url
136
 
                # for future use.
137
 
                self.creds['auth_url'] = auth_url
138
 
                break
139
 
        else:
140
 
            # Guard against a redirection loop
141
 
            raise exception.MaxRedirectsExceeded(redirects=self.MAX_REDIRECTS)
142
 
 
143
 
    def _v1_auth(self, token_url):
144
 
        creds = self.creds
145
 
 
146
 
        headers = {}
147
 
        headers['X-Auth-User'] = creds['username']
148
 
        headers['X-Auth-Key'] = creds['password']
149
 
 
150
 
        tenant = creds.get('tenant')
151
 
        if tenant:
152
 
            headers['X-Auth-Tenant'] = tenant
153
 
 
154
 
        resp, resp_body = self._do_request(token_url, 'GET', headers=headers)
155
 
 
156
 
        def _management_url(self, resp):
157
 
            for url_header in ('x-heat-management-url',
158
 
                               'x-server-management-url',
159
 
                               'x-heat'):
160
 
                try:
161
 
                    return resp[url_header]
162
 
                except KeyError as e:
163
 
                    not_found = e
164
 
            raise not_found
165
 
 
166
 
        if resp.status in (200, 204):
167
 
            try:
168
 
                self.management_url = _management_url(self, resp)
169
 
                self.auth_token = resp['x-auth-token']
170
 
            except KeyError:
171
 
                raise exception.AuthorizationFailure()
172
 
        elif resp.status == 305:
173
 
            raise exception.AuthorizationRedirect(resp['location'])
174
 
        elif resp.status == 400:
175
 
            raise exception.AuthBadRequest(url=token_url)
176
 
        elif resp.status == 401:
177
 
            raise exception.NotAuthorized()
178
 
        elif resp.status == 404:
179
 
            raise exception.AuthUrlNotFound(url=token_url)
180
 
        else:
181
 
            status = resp.status
182
 
            raise Exception(_('Unexpected response: %(status)s')
183
 
                            % {'status': resp.status})
184
 
 
185
 
    def _v2_auth(self, token_url):
186
 
        def get_endpoint(service_catalog):
187
 
            """
188
 
            Select an endpoint from the service catalog
189
 
 
190
 
            We search the full service catalog for services
191
 
            matching both type and region. If the client
192
 
            supplied no region then any endpoint for the service
193
 
            is considered a match. There must be one -- and
194
 
            only one -- successful match in the catalog,
195
 
            otherwise we will raise an exception.
196
 
            """
197
 
            region = self.creds.get('region')
198
 
 
199
 
            service_type_matches = lambda s: s.get('type') == self.service_type
200
 
            region_matches = lambda e: region is None or e['region'] == region
201
 
 
202
 
            endpoints = [ep for s in service_catalog if service_type_matches(s)
203
 
                         for ep in s['endpoints'] if region_matches(ep)]
204
 
 
205
 
            if len(endpoints) > 1:
206
 
                raise exception.RegionAmbiguity(region=region)
207
 
            elif not endpoints:
208
 
                raise exception.NoServiceEndpoint()
209
 
            else:
210
 
                # FIXME(sirp): for now just use the public url.
211
 
                return endpoints[0]['publicURL']
212
 
 
213
 
        creds = self.creds
214
 
 
215
 
        creds = {
216
 
            "auth": {
217
 
                "tenantName": creds['tenant'],
218
 
                "passwordCredentials": {
219
 
                    "username": creds['username'],
220
 
                    "password": creds['password']}}}
221
 
 
222
 
        headers = {}
223
 
        headers['Content-Type'] = 'application/json'
224
 
        req_body = json.dumps(creds)
225
 
 
226
 
        resp, resp_body = self._do_request(
227
 
            token_url, 'POST', headers=headers, body=req_body)
228
 
 
229
 
        if resp.status == 200:
230
 
            resp_auth = json.loads(resp_body)['access']
231
 
            self.management_url = get_endpoint(resp_auth['serviceCatalog'])
232
 
            self.auth_token = resp_auth['token']['id']
233
 
        elif resp.status == 305:
234
 
            raise exception.RedirectException(resp['location'])
235
 
        elif resp.status == 400:
236
 
            raise exception.AuthBadRequest(url=token_url)
237
 
        elif resp.status == 401:
238
 
            raise exception.NotAuthorized()
239
 
        elif resp.status == 404:
240
 
            raise exception.AuthUrlNotFound(url=token_url)
241
 
        else:
242
 
            try:
243
 
                body = json.loads(resp_body)
244
 
                msg = body['error']['message']
245
 
            except (ValueError, KeyError):
246
 
                msg = resp_body
247
 
            raise exception.KeystoneError(resp.status, msg)
248
 
 
249
 
    @property
250
 
    def is_authenticated(self):
251
 
        return self.auth_token is not None
252
 
 
253
 
    @property
254
 
    def strategy(self):
255
 
        return 'keystone'
256
 
 
257
 
    @staticmethod
258
 
    def _do_request(url, method, headers=None, body=None):
259
 
        headers = headers or {}
260
 
        conn = httplib2.Http()
261
 
        conn.force_exception_to_status_code = True
262
 
        headers['User-Agent'] = 'heat-client'
263
 
        resp, resp_body = conn.request(url, method, headers=headers, body=body)
264
 
        return resp, resp_body
265
 
 
266
 
 
267
 
def get_plugin_from_strategy(strategy, creds=None, service_type=None):
268
 
    if strategy == 'noauth':
269
 
        return NoAuthStrategy()
270
 
    elif strategy == 'keystone':
271
 
        return KeystoneStrategy(creds, service_type)
272
 
    else:
273
 
        raise Exception(_("Unknown auth strategy '%s'") % strategy)