1# Licensed under the Apache License, Version 2.0 (the "License"); 2# you may not use this file except in compliance with the License. 3# You may obtain 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, 9# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 10# implied. 11# See the License for the specific language governing permissions and 12# limitations under the License. 13 14import datetime 15import logging 16import re 17from urllib import parse 18 19from django.conf import settings 20from django.contrib import auth 21from django.contrib.auth import models 22from django.utils import timezone 23from keystoneauth1.identity import v3 as v3_auth 24from keystoneauth1 import session 25from keystoneauth1 import token_endpoint 26from keystoneclient.v3 import client as client_v3 27 28from openstack_auth import defaults 29 30LOG = logging.getLogger(__name__) 31 32""" 33We need the request object to get the user, so we'll slightly modify the 34existing django.contrib.auth.get_user method. To do so we update the 35auth middleware to point to our overridden method. 36 37Calling "patch_middleware_get_user" is done in our custom middleware at 38"openstack_auth.middleware" to monkeypatch the code in before it is needed. 39""" 40 41 42def middleware_get_user(request): 43 if not hasattr(request, '_cached_user'): 44 request._cached_user = get_user(request) 45 return request._cached_user 46 47 48def get_user(request): 49 try: 50 user_id = request.session[auth.SESSION_KEY] 51 backend_path = request.session[auth.BACKEND_SESSION_KEY] 52 backend = auth.load_backend(backend_path) 53 backend.request = request 54 user = backend.get_user(user_id) or models.AnonymousUser() 55 except KeyError: 56 user = models.AnonymousUser() 57 return user 58 59 60def patch_middleware_get_user(): 61 # NOTE(adriant): We can't import middleware until our customer user model 62 # is actually registered, otherwise a call to get_user_model within the 63 # middleware module will fail. 64 from django.contrib.auth import middleware 65 middleware.get_user = middleware_get_user 66 auth.get_user = get_user 67 68 69""" End Monkey-Patching. """ 70 71 72def is_token_valid(token, margin=None): 73 """Timezone-aware checking of the auth token's expiration timestamp. 74 75 Returns ``True`` if the token has not yet expired, otherwise ``False``. 76 77 :param token: The openstack_auth.user.Token instance to check 78 79 :param margin: 80 A time margin in seconds to subtract from the real token's validity. 81 An example usage is that the token can be valid once the middleware 82 passed, and invalid (timed-out) during a view rendering and this 83 generates authorization errors during the view rendering. 84 A default margin can be set by the TOKEN_TIMEOUT_MARGIN in the 85 django settings. 86 """ 87 expiration = token.expires 88 # In case we get an unparseable expiration timestamp, return False 89 # so you can't have a "forever" token just by breaking the expires param. 90 if expiration is None: 91 return False 92 if margin is None: 93 margin = settings.TOKEN_TIMEOUT_MARGIN 94 expiration = expiration - datetime.timedelta(seconds=margin) 95 if settings.USE_TZ and timezone.is_naive(expiration): 96 # Presumes that the Keystone is using UTC. 97 expiration = timezone.make_aware(expiration, timezone.utc) 98 return expiration > timezone.now() 99 100 101# NOTE(amotoki): 102# This is a copy from openstack_dashboard.utils.settings.get_dict_config(). 103# This copy is needed to look up defaults for openstack_auth.defaults 104# instead of openstack_dashboard.defaults. 105# TODO(amotoki): This copy might be cleanup if we can use oslo.config 106# for openstack_auth configurations. 107def _get_dict_config(name, key): 108 config = getattr(settings, name) 109 if key in config: 110 return config[key] 111 return getattr(defaults, name)[key] 112 113 114# Helper for figuring out keystone version 115# Implementation will change when API version discovery is available 116def get_keystone_version(): 117 return _get_dict_config('OPENSTACK_API_VERSIONS', 'identity') 118 119 120def get_session(**kwargs): 121 insecure = settings.OPENSTACK_SSL_NO_VERIFY 122 verify = settings.OPENSTACK_SSL_CACERT 123 124 if insecure: 125 verify = False 126 127 return session.Session(verify=verify, **kwargs) 128 129 130def get_keystone_client(): 131 return client_v3 132 133 134def allow_expired_passowrd_change(): 135 """Checks if users should be able to change their expired passwords.""" 136 return getattr(settings, 'ALLOW_USERS_CHANGE_EXPIRED_PASSWORD', True) 137 138 139def build_absolute_uri(request, relative_url): 140 """Ensure absolute_uri are relative to WEBROOT.""" 141 webroot = settings.WEBROOT 142 if webroot.endswith("/") and relative_url.startswith("/"): 143 webroot = webroot[:-1] 144 145 return request.build_absolute_uri(webroot + relative_url) 146 147 148def get_websso_url(request, auth_url, websso_auth): 149 """Return the keystone endpoint for initiating WebSSO. 150 151 Generate the keystone WebSSO endpoint that will redirect the user 152 to the login page of the federated identity provider. 153 154 Based on the authentication type selected by the user in the login 155 form, it will construct the keystone WebSSO endpoint. 156 157 :param request: Django http request object. 158 :type request: django.http.HttpRequest 159 :param auth_url: Keystone endpoint configured in the horizon setting. 160 If WEBSSO_KEYSTONE_URL is defined, its value will be 161 used. Otherwise, the value is derived from: 162 - OPENSTACK_KEYSTONE_URL 163 - AVAILABLE_REGIONS 164 :type auth_url: string 165 :param websso_auth: Authentication type selected by the user from the 166 login form. The value is derived from the horizon 167 setting WEBSSO_CHOICES. 168 :type websso_auth: string 169 170 Example of horizon WebSSO setting:: 171 172 WEBSSO_CHOICES = ( 173 ("credentials", "Keystone Credentials"), 174 ("oidc", "OpenID Connect"), 175 ("saml2", "Security Assertion Markup Language"), 176 ("acme_oidc", "ACME - OpenID Connect"), 177 ("acme_saml2", "ACME - SAML2") 178 ) 179 180 WEBSSO_IDP_MAPPING = { 181 "acme_oidc": ("acme", "oidc"), 182 "acme_saml2": ("acme", "saml2") 183 } 184 } 185 186 The value of websso_auth will be looked up in the WEBSSO_IDP_MAPPING 187 dictionary, if a match is found it will return a IdP specific WebSSO 188 endpoint using the values found in the mapping. 189 190 The value in WEBSSO_IDP_MAPPING is expected to be a tuple formatted as 191 (<idp_id>, <protocol_id>). Using the values found, a IdP/protocol 192 specific URL will be constructed:: 193 194 /auth/OS-FEDERATION/identity_providers/<idp_id> 195 /protocols/<protocol_id>/websso 196 197 If no value is found from the WEBSSO_IDP_MAPPING dictionary, it will 198 treat the value as the global WebSSO protocol <protocol_id> and 199 construct the WebSSO URL by:: 200 201 /auth/OS-FEDERATION/websso/<protocol_id> 202 203 :returns: Keystone WebSSO endpoint. 204 :rtype: string 205 206 """ 207 origin = build_absolute_uri(request, '/auth/websso/') 208 idp_mapping = settings.WEBSSO_IDP_MAPPING 209 idp_id, protocol_id = idp_mapping.get(websso_auth, 210 (None, websso_auth)) 211 212 if idp_id: 213 # Use the IDP specific WebSSO endpoint 214 url = ('%s/auth/OS-FEDERATION/identity_providers/%s' 215 '/protocols/%s/websso?origin=%s' % 216 (auth_url, idp_id, protocol_id, origin)) 217 else: 218 # If no IDP mapping found for the identifier, 219 # perform WebSSO by protocol. 220 url = ('%s/auth/OS-FEDERATION/websso/%s?origin=%s' % 221 (auth_url, protocol_id, origin)) 222 223 return url 224 225 226def has_in_url_path(url, subs): 227 """Test if any of `subs` strings is present in the `url` path.""" 228 scheme, netloc, path, query, fragment = parse.urlsplit(url) 229 return any([sub in path for sub in subs]) 230 231 232def url_path_replace(url, old, new, count=None): 233 """Return a copy of url with replaced path. 234 235 Return a copy of url with all occurrences of old replaced by new in the url 236 path. If the optional argument count is given, only the first count 237 occurrences are replaced. 238 """ 239 args = [] 240 scheme, netloc, path, query, fragment = parse.urlsplit(url) 241 if count is not None: 242 args.append(count) 243 return parse.urlunsplit(( 244 scheme, netloc, path.replace(old, new, *args), query, fragment)) 245 246 247def url_path_append(url, suffix): 248 scheme, netloc, path, query, fragment = parse.urlsplit(url) 249 path = (path + suffix).replace('//', '/') 250 return parse.urlunsplit((scheme, netloc, path, query, fragment)) 251 252 253def _augment_url_with_version(auth_url): 254 """Optionally augment auth_url path with version suffix. 255 256 Check if path component already contains version suffix and if it does 257 not, append version suffix to the end of path, not erasing the previous 258 path contents, since keystone web endpoint (like /identity) could be 259 there. Keystone version needs to be added to endpoint because as of Kilo, 260 the identity URLs returned by Keystone might no longer contain API 261 versions, leaving the version choice up to the user. 262 """ 263 if has_in_url_path(auth_url, ["/v3"]): 264 return auth_url 265 266 return url_path_append(auth_url, "/v3") 267 268 269def fix_auth_url_version_prefix(auth_url): 270 """Fix up the auth url if an invalid or no version prefix was given. 271 272 Fix the URL to say v3 in this case and add version if it is 273 missing entirely. This should be smarter and use discovery. 274 Until version discovery is implemented we need this method to get 275 everything working. 276 """ 277 auth_url = _augment_url_with_version(auth_url) 278 279 url_fixed = False 280 if has_in_url_path(auth_url, ["/v2.0"]): 281 url_fixed = True 282 auth_url = url_path_replace(auth_url, "/v2.0", "/v3", 1) 283 284 return auth_url, url_fixed 285 286 287def clean_up_auth_url(auth_url): 288 """Clean up the auth url to extract the exact Keystone URL""" 289 290 # NOTE(mnaser): This drops the query and fragment because we're only 291 # trying to extract the Keystone URL. 292 scheme, netloc, path, query, fragment = parse.urlsplit(auth_url) 293 return parse.urlunsplit(( 294 scheme, netloc, re.sub(r'/auth.*', '', path), '', '')) 295 296 297def get_token_auth_plugin(auth_url, token, project_id=None, domain_name=None): 298 if domain_name: 299 return v3_auth.Token(auth_url=auth_url, 300 token=token, 301 domain_name=domain_name, 302 reauthenticate=False) 303 return v3_auth.Token(auth_url=auth_url, 304 token=token, 305 project_id=project_id, 306 reauthenticate=False) 307 308 309def get_project_list(*args, **kwargs): 310 is_federated = kwargs.get('is_federated', False) 311 sess = kwargs.get('session') or get_session() 312 auth_url, _ = fix_auth_url_version_prefix(kwargs['auth_url']) 313 auth = token_endpoint.Token(auth_url, kwargs['token']) 314 client = get_keystone_client().Client(session=sess, auth=auth) 315 316 if is_federated: 317 projects = client.federation.projects.list() 318 else: 319 projects = client.projects.list(user=kwargs.get('user_id')) 320 321 projects.sort(key=lambda project: project.name.lower()) 322 return projects 323 324 325def default_services_region(service_catalog, request=None, 326 ks_endpoint=None): 327 """Return the default service region. 328 329 Order of precedence: 330 1. 'services_region' cookie value 331 2. Matching endpoint in DEFAULT_SERVICE_REGIONS 332 3. '*' key in DEFAULT_SERVICE_REGIONS 333 4. First valid region from catalog 334 335 In each case the value must also be present in available_regions or 336 we move to the next level of precedence. 337 """ 338 if service_catalog: 339 available_regions = [get_endpoint_region(endpoint) for service 340 in service_catalog for endpoint 341 in service.get('endpoints', []) 342 if (service.get('type') is not None and 343 service.get('type') != 'identity')] 344 if not available_regions: 345 # this is very likely an incomplete keystone setup 346 LOG.warning('No regions could be found excluding identity.') 347 available_regions = [get_endpoint_region(endpoint) for service 348 in service_catalog for endpoint 349 in service.get('endpoints', [])] 350 351 if not available_regions: 352 # if there are no region setup for any service endpoint, 353 # this is a critical problem and it's not clear how this occurs 354 LOG.error('No regions can be found in the service catalog.') 355 return None 356 357 region_options = [] 358 if request: 359 region_options.append(request.COOKIES.get('services_region')) 360 if ks_endpoint: 361 default_service_regions = settings.DEFAULT_SERVICE_REGIONS 362 region_options.append(default_service_regions.get(ks_endpoint)) 363 region_options.append(settings.DEFAULT_SERVICE_REGIONS.get('*')) 364 365 for region in region_options: 366 if region in available_regions: 367 return region 368 return available_regions[0] 369 return None 370 371 372def set_response_cookie(response, cookie_name, cookie_value): 373 """Common function for setting the cookie in the response. 374 375 Provides a common policy of setting cookies for last used project 376 and region, can be reused in other locations. 377 378 This method will set the cookie to expire in 365 days. 379 """ 380 now = timezone.now() 381 expire_date = now + datetime.timedelta(days=365) 382 response.set_cookie(cookie_name, cookie_value, expires=expire_date) 383 384 385def get_endpoint_region(endpoint): 386 """Common function for getting the region from endpoint. 387 388 In Keystone V3, region has been deprecated in favor of 389 region_id. 390 391 This method provides a way to get region that works for both 392 Keystone V2 and V3. 393 """ 394 return endpoint.get('region_id') or endpoint.get('region') 395 396 397def using_cookie_backed_sessions(): 398 engine = settings.SESSION_ENGINE 399 return "signed_cookies" in engine 400 401 402def get_admin_roles(): 403 """Common function for getting the admin roles from settings 404 405 :return: 406 Set object including all admin roles. 407 If there is no role, this will return empty:: 408 409 { 410 "foo", "bar", "admin" 411 } 412 413 """ 414 admin_roles = {role.lower() for role 415 in settings.OPENSTACK_KEYSTONE_ADMIN_ROLES} 416 return admin_roles 417 418 419def get_role_permission(role): 420 """Common function for getting the permission froms arg 421 422 This format is 'openstack.roles.xxx' and 'xxx' is a real role name. 423 424 :returns: 425 String like "openstack.roles.admin" 426 If role is None, this will return None. 427 428 """ 429 return "openstack.roles.%s" % role.lower() 430 431 432def get_admin_permissions(): 433 """Common function for getting the admin permissions from settings 434 435 This format is 'openstack.roles.xxx' and 'xxx' is a real role name. 436 437 :returns: 438 Set object including all admin permission. 439 If there is no permission, this will return empty:: 440 441 { 442 "openstack.roles.foo", 443 "openstack.roles.bar", 444 "openstack.roles.admin" 445 } 446 447 """ 448 return {get_role_permission(role) for role in get_admin_roles()} 449 450 451def get_client_ip(request): 452 """Return client ip address using SECURE_PROXY_ADDR_HEADER variable. 453 454 If not present or not defined on settings then REMOTE_ADDR is used. 455 456 :param request: Django http request object. 457 :type request: django.http.HttpRequest 458 459 :returns: Possible client ip address 460 :rtype: string 461 """ 462 _SECURE_PROXY_ADDR_HEADER = settings.SECURE_PROXY_ADDR_HEADER 463 if _SECURE_PROXY_ADDR_HEADER: 464 return request.META.get( 465 _SECURE_PROXY_ADDR_HEADER, 466 request.META.get('REMOTE_ADDR') 467 ) 468 return request.META.get('REMOTE_ADDR') 469 470 471def store_initial_k2k_session(auth_url, request, scoped_auth_ref, 472 unscoped_auth_ref): 473 """Stores session variables if there are k2k service providers 474 475 This stores variables related to Keystone2Keystone federation. This 476 function gets skipped if there are no Keystone service providers. 477 An unscoped token to the identity provider keystone gets stored 478 so that it can be used to do federated login into the service 479 providers when switching keystone providers. 480 The settings file can be configured to set the display name 481 of the local (identity provider) keystone by setting 482 KEYSTONE_PROVIDER_IDP_NAME. The KEYSTONE_PROVIDER_IDP_ID settings 483 variable is used for comparison against the service providers. 484 It should not conflict with any of the service provider ids. 485 486 :param auth_url: base token auth url 487 :param request: Django http request object 488 :param scoped_auth_ref: Scoped Keystone access info object 489 :param unscoped_auth_ref: Unscoped Keystone access info object 490 """ 491 keystone_provider_id = request.session.get('keystone_provider_id', None) 492 if keystone_provider_id: 493 return None 494 495 providers = getattr(scoped_auth_ref, 'service_providers', None) 496 if providers: 497 providers = getattr(providers, '_service_providers', None) 498 499 if providers: 500 keystone_idp_name = settings.KEYSTONE_PROVIDER_IDP_NAME 501 keystone_idp_id = settings.KEYSTONE_PROVIDER_IDP_ID 502 keystone_identity_provider = {'name': keystone_idp_name, 503 'id': keystone_idp_id} 504 # (edtubill) We will use the IDs as the display names 505 # We may want to be able to set display names in the future. 506 keystone_providers = [ 507 {'name': provider_id, 'id': provider_id} 508 for provider_id in providers] 509 510 keystone_providers.append(keystone_identity_provider) 511 512 # We treat the Keystone idp ID as None 513 request.session['keystone_provider_id'] = keystone_idp_id 514 request.session['keystone_providers'] = keystone_providers 515 request.session['k2k_base_unscoped_token'] =\ 516 unscoped_auth_ref.auth_token 517 request.session['k2k_auth_url'] = auth_url 518