1 # Copyright 2011 OpenStack Foundation
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
8 # http://www.apache.org/licenses/LICENSE-2.0
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
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).
21 > auth_plugin = AuthPlugin(creds)
23 > auth_plugin.authenticate()
25 > auth_plugin.auth_token
28 > auth_plugin.management_url
29 http://service_endpoint/
32 from oslo.serialization import jsonutils
33 from oslo.log import log as logging
34 # NOTE(jokke): simplified transition to py3, behaves like py2 xrange
35 from six.moves import range
36 import six.moves.urllib.parse as urlparse
38 from escalator.common import exception
39 from escalator import i18n
42 LOG = logging.getLogger(__name__)
46 class BaseStrategy(object):
49 self.auth_token = None
50 # TODO(sirp): Should expose selecting public/internal/admin URL.
51 self.management_url = None
53 def authenticate(self):
54 raise NotImplementedError
57 def is_authenticated(self):
58 raise NotImplementedError
62 raise NotImplementedError
65 class NoAuthStrategy(BaseStrategy):
67 def authenticate(self):
71 def is_authenticated(self):
79 class KeystoneStrategy(BaseStrategy):
82 def __init__(self, creds, insecure=False, configure_via_auth=True):
84 self.insecure = insecure
85 self.configure_via_auth = configure_via_auth
86 super(KeystoneStrategy, self).__init__()
88 def check_auth_params(self):
89 # Ensure that supplied credential parameters are as required
90 for required in ('username', 'password', 'auth_url',
92 if self.creds.get(required) is None:
93 raise exception.MissingCredentialError(required=required)
94 if self.creds['strategy'] != 'keystone':
95 raise exception.BadAuthStrategy(expected='keystone',
96 received=self.creds['strategy'])
97 # For v2.0 also check tenant is present
98 if self.creds['auth_url'].rstrip('/').endswith('v2.0'):
99 if self.creds.get("tenant") is None:
100 raise exception.MissingCredentialError(required='tenant')
102 def authenticate(self):
103 """Authenticate with the Keystone service.
105 There are a few scenarios to consider here:
107 1. Which version of Keystone are we using? v1 which uses headers to
108 pass the credentials, or v2 which uses a JSON encoded request body?
110 2. Keystone may respond back with a redirection using a 305 status
113 3. We may attempt a v1 auth when v2 is what's called for. In this
114 case, we rewrite the url to contain /v2.0/ and retry using the v2
117 def _authenticate(auth_url):
118 # If OS_AUTH_URL is missing a trailing slash add one
119 if not auth_url.endswith('/'):
121 token_url = urlparse.urljoin(auth_url, "tokens")
122 # 1. Check Keystone version
123 is_v2 = auth_url.rstrip('/').endswith('v2.0')
125 self._v2_auth(token_url)
127 self._v1_auth(token_url)
129 self.check_auth_params()
130 auth_url = self.creds['auth_url']
131 for _ in range(self.MAX_REDIRECTS):
133 _authenticate(auth_url)
134 except exception.AuthorizationRedirect as e:
135 # 2. Keystone may redirect us
137 except exception.AuthorizationFailure:
138 # 3. In some configurations nova makes redirection to
139 # v2.0 keystone endpoint. Also, new location does not
140 # contain real endpoint, only hostname and port.
141 if 'v2.0' not in auth_url:
142 auth_url = urlparse.urljoin(auth_url, 'v2.0/')
144 # If we successfully auth'd, then memorize the correct auth_url
146 self.creds['auth_url'] = auth_url
149 # Guard against a redirection loop
150 raise exception.MaxRedirectsExceeded(redirects=self.MAX_REDIRECTS)
152 def _v1_auth(self, token_url):
156 headers['X-Auth-User'] = creds['username']
157 headers['X-Auth-Key'] = creds['password']
159 tenant = creds.get('tenant')
161 headers['X-Auth-Tenant'] = tenant
163 resp, resp_body = self._do_request(token_url, 'GET', headers=headers)
165 def _management_url(self, resp):
166 for url_header in ('x-image-management-url',
167 'x-server-management-url',
170 return resp[url_header]
171 except KeyError as e:
175 if resp.status in (200, 204):
177 if self.configure_via_auth:
178 self.management_url = _management_url(self, resp)
179 self.auth_token = resp['x-auth-token']
181 raise exception.AuthorizationFailure()
182 elif resp.status == 305:
183 raise exception.AuthorizationRedirect(uri=resp['location'])
184 elif resp.status == 400:
185 raise exception.AuthBadRequest(url=token_url)
186 elif resp.status == 401:
187 raise exception.NotAuthenticated()
188 elif resp.status == 404:
189 raise exception.AuthUrlNotFound(url=token_url)
191 raise Exception(_('Unexpected response: %s') % resp.status)
193 def _v2_auth(self, token_url):
199 "tenantName": creds['tenant'],
200 "passwordCredentials": {
201 "username": creds['username'],
202 "password": creds['password']
208 headers['Content-Type'] = 'application/json'
209 req_body = jsonutils.dumps(creds)
211 resp, resp_body = self._do_request(
212 token_url, 'POST', headers=headers, body=req_body)
214 if resp.status == 200:
215 resp_auth = jsonutils.loads(resp_body)['access']
216 creds_region = self.creds.get('region')
217 if self.configure_via_auth:
218 endpoint = get_endpoint(resp_auth['serviceCatalog'],
219 endpoint_region=creds_region)
220 self.management_url = endpoint
221 self.auth_token = resp_auth['token']['id']
222 elif resp.status == 305:
223 raise exception.RedirectException(resp['location'])
224 elif resp.status == 400:
225 raise exception.AuthBadRequest(url=token_url)
226 elif resp.status == 401:
227 raise exception.NotAuthenticated()
228 elif resp.status == 404:
229 raise exception.AuthUrlNotFound(url=token_url)
231 raise Exception(_('Unexpected response: %s') % resp.status)
234 def is_authenticated(self):
235 return self.auth_token is not None
241 def _do_request(self, url, method, headers=None, body=None):
242 headers = headers or {}
243 conn = httplib2.Http()
244 conn.force_exception_to_status_code = True
245 conn.disable_ssl_certificate_validation = self.insecure
246 headers['User-Agent'] = 'escalator-client'
247 resp, resp_body = conn.request(url, method, headers=headers, body=body)
248 return resp, resp_body
251 def get_plugin_from_strategy(strategy, creds=None, insecure=False,
252 configure_via_auth=True):
253 if strategy == 'noauth':
254 return NoAuthStrategy()
255 elif strategy == 'keystone':
256 return KeystoneStrategy(creds, insecure,
257 configure_via_auth=configure_via_auth)
259 raise Exception(_("Unknown auth strategy '%s'") % strategy)
262 def get_endpoint(service_catalog, service_type='image', endpoint_region=None,
263 endpoint_type='publicURL'):
265 Select an endpoint from the service catalog
267 We search the full service catalog for services
268 matching both type and region. If the client
269 supplied no region then any 'image' endpoint
270 is considered a match. There must be one -- and
271 only one -- successful match in the catalog,
272 otherwise we will raise an exception.
275 for service in service_catalog:
278 s_type = service['type']
280 msg = _('Encountered service with no "type": %s') % s_type
284 if s_type == service_type:
285 for ep in service['endpoints']:
286 if endpoint_region is None or endpoint_region == ep['region']:
287 if endpoint is not None:
288 # This is a second match, abort
289 raise exception.RegionAmbiguity(region=endpoint_region)
291 if endpoint and endpoint.get(endpoint_type):
292 return endpoint[endpoint_type]
294 raise exception.NoServiceEndpoint()