1# Licensed under the Apache License, Version 2.0 (the "License"); you may 2# not use this file except in compliance with the License. You may obtain 3# a copy of the License at 4# 5# http://www.apache.org/licenses/LICENSE-2.0 6# 7# Unless required by applicable law or agreed to in writing, software 8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10# License for the specific language governing permissions and limitations 11# under the License. 12 13import abc 14import json 15 16import six 17 18from keystoneauth1 import _utils as utils 19from keystoneauth1 import access 20from keystoneauth1 import exceptions 21from keystoneauth1.identity import base 22 23_logger = utils.get_logger(__name__) 24 25__all__ = ('Auth', 'AuthMethod', 'AuthConstructor', 'BaseAuth') 26 27 28@six.add_metaclass(abc.ABCMeta) 29class BaseAuth(base.BaseIdentityPlugin): 30 """Identity V3 Authentication Plugin. 31 32 :param string auth_url: Identity service endpoint for authentication. 33 :param string trust_id: Trust ID for trust scoping. 34 :param string system_scope: System information to scope to. 35 :param string domain_id: Domain ID for domain scoping. 36 :param string domain_name: Domain name for domain scoping. 37 :param string project_id: Project ID for project scoping. 38 :param string project_name: Project name for project scoping. 39 :param string project_domain_id: Project's domain ID for project. 40 :param string project_domain_name: Project's domain name for project. 41 :param bool reauthenticate: Allow fetching a new token if the current one 42 is going to expire. (optional) default True 43 :param bool include_catalog: Include the service catalog in the returned 44 token. (optional) default True. 45 """ 46 47 def __init__(self, auth_url, 48 trust_id=None, 49 system_scope=None, 50 domain_id=None, 51 domain_name=None, 52 project_id=None, 53 project_name=None, 54 project_domain_id=None, 55 project_domain_name=None, 56 reauthenticate=True, 57 include_catalog=True): 58 super(BaseAuth, self).__init__(auth_url=auth_url, 59 reauthenticate=reauthenticate) 60 self.trust_id = trust_id 61 self.system_scope = system_scope 62 self.domain_id = domain_id 63 self.domain_name = domain_name 64 self.project_id = project_id 65 self.project_name = project_name 66 self.project_domain_id = project_domain_id 67 self.project_domain_name = project_domain_name 68 self.include_catalog = include_catalog 69 70 @property 71 def token_url(self): 72 """The full URL where we will send authentication data.""" 73 return '%s/auth/tokens' % self.auth_url.rstrip('/') 74 75 @abc.abstractmethod 76 def get_auth_ref(self, session, **kwargs): 77 return None 78 79 @property 80 def has_scope_parameters(self): 81 """Return true if parameters can be used to create a scoped token.""" 82 return (self.domain_id or self.domain_name or 83 self.project_id or self.project_name or 84 self.trust_id or self.system_scope) 85 86 87class Auth(BaseAuth): 88 """Identity V3 Authentication Plugin. 89 90 :param string auth_url: Identity service endpoint for authentication. 91 :param list auth_methods: A collection of methods to authenticate with. 92 :param string trust_id: Trust ID for trust scoping. 93 :param string domain_id: Domain ID for domain scoping. 94 :param string domain_name: Domain name for domain scoping. 95 :param string project_id: Project ID for project scoping. 96 :param string project_name: Project name for project scoping. 97 :param string project_domain_id: Project's domain ID for project. 98 :param string project_domain_name: Project's domain name for project. 99 :param bool reauthenticate: Allow fetching a new token if the current one 100 is going to expire. (optional) default True 101 :param bool include_catalog: Include the service catalog in the returned 102 token. (optional) default True. 103 :param bool unscoped: Force the return of an unscoped token. This will make 104 the keystone server return an unscoped token even if 105 a default_project_id is set for this user. 106 """ 107 108 def __init__(self, auth_url, auth_methods, **kwargs): 109 self.unscoped = kwargs.pop('unscoped', False) 110 super(Auth, self).__init__(auth_url=auth_url, **kwargs) 111 self.auth_methods = auth_methods 112 113 def add_method(self, method): 114 """Add an additional initialized AuthMethod instance.""" 115 self.auth_methods.append(method) 116 117 def get_auth_ref(self, session, **kwargs): 118 headers = {'Accept': 'application/json'} 119 body = {'auth': {'identity': {}}} 120 ident = body['auth']['identity'] 121 rkwargs = {} 122 123 for method in self.auth_methods: 124 name, auth_data = method.get_auth_data( 125 session, self, headers, request_kwargs=rkwargs) 126 # NOTE(adriant): Methods like ReceiptMethod don't 127 # want anything added to the request data, so they 128 # explicitly return None, which we check for. 129 if name: 130 ident.setdefault('methods', []).append(name) 131 ident[name] = auth_data 132 133 if not ident: 134 raise exceptions.AuthorizationFailure( 135 'Authentication method required (e.g. password)') 136 137 mutual_exclusion = [bool(self.domain_id or self.domain_name), 138 bool(self.project_id or self.project_name), 139 bool(self.trust_id), 140 bool(self.unscoped)] 141 142 if sum(mutual_exclusion) > 1: 143 raise exceptions.AuthorizationFailure( 144 message='Authentication cannot be scoped to multiple' 145 ' targets. Pick one of: project, domain, ' 146 'trust or unscoped') 147 148 if self.domain_id: 149 body['auth']['scope'] = {'domain': {'id': self.domain_id}} 150 elif self.domain_name: 151 body['auth']['scope'] = {'domain': {'name': self.domain_name}} 152 elif self.project_id: 153 body['auth']['scope'] = {'project': {'id': self.project_id}} 154 elif self.project_name: 155 scope = body['auth']['scope'] = {'project': {}} 156 scope['project']['name'] = self.project_name 157 158 if self.project_domain_id: 159 scope['project']['domain'] = {'id': self.project_domain_id} 160 elif self.project_domain_name: 161 scope['project']['domain'] = {'name': self.project_domain_name} 162 elif self.trust_id: 163 body['auth']['scope'] = {'OS-TRUST:trust': {'id': self.trust_id}} 164 elif self.unscoped: 165 body['auth']['scope'] = 'unscoped' 166 elif self.system_scope: 167 # NOTE(lbragstad): Right now it's only possible to have role 168 # assignments on the entire system. In the future that might change 169 # so that users and groups can have roles on parts of the system, 170 # like a specific service in a specific region. If that happens, 171 # this will have to be accounted for here. Until then we'll only 172 # support scoping to the entire system. 173 if self.system_scope == 'all': 174 body['auth']['scope'] = {'system': {'all': True}} 175 176 token_url = self.token_url 177 178 if not self.auth_url.rstrip('/').endswith('v3'): 179 token_url = '%s/v3/auth/tokens' % self.auth_url.rstrip('/') 180 181 # NOTE(jamielennox): we add nocatalog here rather than in token_url 182 # directly as some federation plugins require the base token_url 183 if not self.include_catalog: 184 token_url += '?nocatalog' 185 186 _logger.debug('Making authentication request to %s', token_url) 187 resp = session.post(token_url, json=body, headers=headers, 188 authenticated=False, log=False, **rkwargs) 189 190 try: 191 _logger.debug(json.dumps(resp.json())) 192 resp_data = resp.json() 193 except ValueError: 194 raise exceptions.InvalidResponse(response=resp) 195 196 if 'token' not in resp_data: 197 raise exceptions.InvalidResponse(response=resp) 198 199 return access.AccessInfoV3(auth_token=resp.headers['X-Subject-Token'], 200 body=resp_data) 201 202 def get_cache_id_elements(self): 203 if not self.auth_methods: 204 return None 205 206 params = {'auth_url': self.auth_url, 207 'domain_id': self.domain_id, 208 'domain_name': self.domain_name, 209 'project_id': self.project_id, 210 'project_name': self.project_name, 211 'project_domain_id': self.project_domain_id, 212 'project_domain_name': self.project_domain_name, 213 'trust_id': self.trust_id} 214 215 for method in self.auth_methods: 216 try: 217 elements = method.get_cache_id_elements() 218 except NotImplementedError: 219 return None 220 221 params.update(elements) 222 223 return params 224 225 226@six.add_metaclass(abc.ABCMeta) 227class AuthMethod(object): 228 """One part of a V3 Authentication strategy. 229 230 V3 Tokens allow multiple methods to be presented when authentication 231 against the server. Each one of these methods is implemented by an 232 AuthMethod. 233 234 Note: When implementing an AuthMethod use the method_parameters 235 and do not use positional arguments. Otherwise they can't be picked up by 236 the factory method and don't work as well with AuthConstructors. 237 """ 238 239 _method_parameters = [] 240 241 def __init__(self, **kwargs): 242 for param in self._method_parameters: 243 setattr(self, param, kwargs.pop(param, None)) 244 245 if kwargs: 246 msg = "Unexpected Attributes: %s" % ", ".join(kwargs.keys()) 247 raise AttributeError(msg) 248 249 @classmethod 250 def _extract_kwargs(cls, kwargs): 251 """Remove parameters related to this method from other kwargs.""" 252 return dict([(p, kwargs.pop(p, None)) 253 for p in cls._method_parameters]) 254 255 @abc.abstractmethod 256 def get_auth_data(self, session, auth, headers, **kwargs): 257 """Return the authentication section of an auth plugin. 258 259 :param session: The communication session. 260 :type session: keystoneauth1.session.Session 261 :param base.Auth auth: The auth plugin calling the method. 262 :param dict headers: The headers that will be sent with the auth 263 request if a plugin needs to add to them. 264 :return: The identifier of this plugin and a dict of authentication 265 data for the auth type. 266 :rtype: tuple(string, dict) 267 """ 268 269 def get_cache_id_elements(self): 270 """Get the elements for this auth method that make it unique. 271 272 These elements will be used as part of the 273 :py:meth:`keystoneauth1.plugin.BaseIdentityPlugin.get_cache_id` to 274 allow caching of the auth plugin. 275 276 Plugins should override this if they want to allow caching of their 277 state. 278 279 To avoid collision or overrides the keys of the returned dictionary 280 should be prefixed with the plugin identifier. For example the password 281 plugin returns its username value as 'password_username'. 282 """ 283 raise NotImplementedError() 284 285 286@six.add_metaclass(abc.ABCMeta) 287class AuthConstructor(Auth): 288 """Abstract base class for creating an Auth Plugin. 289 290 The Auth Plugin created contains only one authentication method. This 291 is generally the required usage. 292 293 An AuthConstructor creates an AuthMethod based on the method's 294 arguments and the auth_method_class defined by the plugin. It then 295 creates the auth plugin with only that authentication method. 296 """ 297 298 _auth_method_class = None 299 300 def __init__(self, auth_url, *args, **kwargs): 301 method_kwargs = self._auth_method_class._extract_kwargs(kwargs) 302 method = self._auth_method_class(*args, **method_kwargs) 303 super(AuthConstructor, self).__init__(auth_url, [method], **kwargs) 304