keystone  18.0.0
About: OpenStack Keystone (Core Service: Identity) provides an authentication and authorization service for other OpenStack services. Provides a catalog of endpoints for all OpenStack services.
The "Victoria" series (maintained release).
  Fossies Dox: keystone-18.0.0.tar.gz  ("unofficial" and yet experimental doxygen-generated source code documentation)  

utils.py
Go to the documentation of this file.
1 # Copyright 2012 OpenStack Foundation
2 # Copyright 2010 United States Government as represented by the
3 # Administrator of the National Aeronautics and Space Administration.
4 # Copyright 2011 - 2012 Justin Santa Barbara
5 # All Rights Reserved.
6 #
7 # Licensed under the Apache License, Version 2.0 (the "License"); you may
8 # not use this file except in compliance with the License. You may obtain
9 # a copy of the License at
10 #
11 # http://www.apache.org/licenses/LICENSE-2.0
12 #
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16 # License for the specific language governing permissions and limitations
17 # under the License.
18 
19 import collections
20 import grp
21 import hashlib
22 import itertools
23 import os
24 import pwd
25 import uuid
26 
27 from oslo_log import log
28 from oslo_serialization import jsonutils
29 from oslo_utils import reflection
30 from oslo_utils import strutils
31 from oslo_utils import timeutils
32 import urllib
33 
34 
35 from keystone.common import password_hashing
36 import keystone.conf
37 from keystone import exception
38 from keystone.i18n import _
39 
40 
41 CONF = keystone.conf.CONF
42 LOG = log.getLogger(__name__)
43 WHITELISTED_PROPERTIES = [
44  'tenant_id', 'project_id', 'user_id',
45  'public_bind_host', 'admin_bind_host',
46  'compute_host', 'admin_port', 'public_port',
47  'public_endpoint', ]
48 
49 
50 # NOTE(stevermar): This UUID must stay the same, forever, across
51 # all of keystone to preserve its value as a URN namespace, which is
52 # used for ID transformation.
53 RESOURCE_ID_NAMESPACE = uuid.UUID('4332ecab-770b-4288-a680-b9aca3b1b153')
54 
55 # Compatibilty for password hashing functions.
56 verify_length_and_trunc_password = password_hashing.verify_length_and_trunc_password # noqa
57 hash_password = password_hashing.hash_password
58 hash_user_password = password_hashing.hash_user_password
59 check_password = password_hashing.check_password
60 
61 
62 def resource_uuid(value):
63  """Convert input to valid UUID hex digits."""
64  try:
65  uuid.UUID(value)
66  return value
67  except ValueError:
68  if len(value) <= 64:
69  return uuid.uuid5(RESOURCE_ID_NAMESPACE, value).hex
70  raise ValueError(_('Length of transformable resource id > 64, '
71  'which is max allowed characters'))
72 
73 
74 def flatten_dict(d, parent_key=''):
75  """Flatten a nested dictionary.
76 
77  Converts a dictionary with nested values to a single level flat
78  dictionary, with dotted notation for each key.
79 
80  """
81  items = []
82  for k, v in d.items():
83  new_key = parent_key + '.' + k if parent_key else k
84  if isinstance(v, collections.MutableMapping):
85  items.extend(list(flatten_dict(v, new_key).items()))
86  else:
87  items.append((new_key, v))
88  return dict(items)
89 
90 
91 class SmarterEncoder(jsonutils.json.JSONEncoder):
92  """Help for JSON encoding dict-like objects."""
93 
94  def default(self, obj):
95  if not isinstance(obj, dict) and hasattr(obj, 'items'):
96  return dict(obj.items())
97  return super(SmarterEncoder, self).default(obj)
98 
99 
100 def hash_access_key(access):
101  hash_ = hashlib.sha256()
102  if not isinstance(access, bytes):
103  access = access.encode('utf-8')
104  hash_.update(access)
105  return hash_.hexdigest()
106 
107 
108 def attr_as_boolean(val_attr):
109  """Return the boolean value, decoded from a string.
110 
111  We test explicitly for a value meaning False, which can be one of
112  several formats as specified in oslo strutils.FALSE_STRINGS.
113  All other string values (including an empty string) are treated as
114  meaning True.
115 
116  """
117  return strutils.bool_from_string(val_attr, default=True)
118 
119 
120 def auth_str_equal(provided, known):
121  """Constant-time string comparison.
122 
123  :params provided: the first string
124  :params known: the second string
125 
126  :returns: True if the strings are equal.
127 
128  This function takes two strings and compares them. It is intended to be
129  used when doing a comparison for authentication purposes to help guard
130  against timing attacks. When using the function for this purpose, always
131  provide the user-provided password as the first argument. The time this
132  function will take is always a factor of the length of this string.
133  """
134  result = 0
135  p_len = len(provided)
136  k_len = len(known)
137  for i in range(p_len):
138  a = ord(provided[i]) if i < p_len else 0
139  b = ord(known[i]) if i < k_len else 0
140  result |= a ^ b
141  return (p_len == k_len) & (result == 0)
142 
143 
145  if CONF.pydev_debug_host and CONF.pydev_debug_port:
146  try:
147  try:
148  from pydev import pydevd
149  except ImportError:
150  import pydevd
151 
152  pydevd.settrace(CONF.pydev_debug_host,
153  port=CONF.pydev_debug_port,
154  stdoutToServer=True,
155  stderrToServer=True)
156  return True
157  except Exception:
158  LOG.exception(
159  'Error setting up the debug environment. Verify that the '
160  'option --debug-url has the format <host>:<port> and that a '
161  'debugger processes is listening on that port.')
162  raise
163 
164 
165 def get_unix_user(user=None):
166  """Get the uid and user name.
167 
168  This is a convenience utility which accepts a variety of input
169  which might represent a unix user. If successful it returns the uid
170  and name. Valid input is:
171 
172  string
173  A string is first considered to be a user name and a lookup is
174  attempted under that name. If no name is found then an attempt
175  is made to convert the string to an integer and perform a
176  lookup as a uid.
177 
178  int
179  An integer is interpreted as a uid.
180 
181  None
182  None is interpreted to mean use the current process's
183  effective user.
184 
185  If the input is a valid type but no user is found a KeyError is
186  raised. If the input is not a valid type a TypeError is raised.
187 
188  :param object user: string, int or None specifying the user to
189  lookup.
190 
191  :returns: tuple of (uid, name)
192 
193  """
194  if isinstance(user, str):
195  try:
196  user_info = pwd.getpwnam(user)
197  except KeyError:
198  try:
199  i = int(user)
200  except ValueError:
201  raise KeyError("user name '%s' not found" % user)
202  try:
203  user_info = pwd.getpwuid(i)
204  except KeyError:
205  raise KeyError("user id %d not found" % i)
206  elif isinstance(user, int):
207  try:
208  user_info = pwd.getpwuid(user)
209  except KeyError:
210  raise KeyError("user id %d not found" % user)
211  elif user is None:
212  user_info = pwd.getpwuid(os.geteuid())
213  else:
214  user_cls_name = reflection.get_class_name(user,
215  fully_qualified=False)
216  raise TypeError('user must be string, int or None; not %s (%r)' %
217  (user_cls_name, user))
218 
219  return user_info.pw_uid, user_info.pw_name
220 
221 
222 def get_unix_group(group=None):
223  """Get the gid and group name.
224 
225  This is a convenience utility which accepts a variety of input
226  which might represent a unix group. If successful it returns the gid
227  and name. Valid input is:
228 
229  string
230  A string is first considered to be a group name and a lookup is
231  attempted under that name. If no name is found then an attempt
232  is made to convert the string to an integer and perform a
233  lookup as a gid.
234 
235  int
236  An integer is interpreted as a gid.
237 
238  None
239  None is interpreted to mean use the current process's
240  effective group.
241 
242  If the input is a valid type but no group is found a KeyError is
243  raised. If the input is not a valid type a TypeError is raised.
244 
245 
246  :param object group: string, int or None specifying the group to
247  lookup.
248 
249  :returns: tuple of (gid, name)
250 
251  """
252  if isinstance(group, str):
253  try:
254  group_info = grp.getgrnam(group)
255  except KeyError:
256  # Was an int passed as a string?
257  # Try converting to int and lookup by id instead.
258  try:
259  i = int(group)
260  except ValueError:
261  raise KeyError("group name '%s' not found" % group)
262  try:
263  group_info = grp.getgrgid(i)
264  except KeyError:
265  raise KeyError("group id %d not found" % i)
266  elif isinstance(group, int):
267  try:
268  group_info = grp.getgrgid(group)
269  except KeyError:
270  raise KeyError("group id %d not found" % group)
271  elif group is None:
272  group_info = grp.getgrgid(os.getegid())
273  else:
274  group_cls_name = reflection.get_class_name(group,
275  fully_qualified=False)
276  raise TypeError('group must be string, int or None; not %s (%r)' %
277  (group_cls_name, group))
278 
279  return group_info.gr_gid, group_info.gr_name
280 
281 
282 class WhiteListedItemFilter(object):
283 
284  def __init__(self, whitelist, data):
285  self._whitelist = set(whitelist or [])
286  self._data = data
287 
288  def __getitem__(self, name):
289  """Evaluation on an item access."""
290  if name not in self._whitelist:
291  raise KeyError
292  return self._data[name]
293 
294 
295 _ISO8601_TIME_FORMAT_SUBSECOND = '%Y-%m-%dT%H:%M:%S.%f'
296 _ISO8601_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S'
297 
298 
299 def isotime(at=None, subsecond=False):
300  """Stringify time in ISO 8601 format.
301 
302  Python provides a similar instance method for datetime.datetime objects
303  called `isoformat()`. The format of the strings generated by `isoformat()`
304  has a couple of problems:
305 
306  1) The strings generated by `isotime()` are used in tokens and other public
307  APIs that we can't change without a deprecation period. The strings
308  generated by `isoformat()` are not the same format, so we can't just
309  change to it.
310 
311  2) The strings generated by `isoformat()` do not include the microseconds
312  if the value happens to be 0. This will likely show up as random
313  failures as parsers may be written to always expect microseconds, and it
314  will parse correctly most of the time.
315 
316  :param at: Optional datetime object to return at a string. If not provided,
317  the time when the function was called will be used.
318  :type at: datetime.datetime
319  :param subsecond: If true, the returned string will represent microsecond
320  precision, but only precise to the second. For example, a
321  `datetime.datetime(2016, 9, 14, 14, 1, 13, 970223)` will
322  be returned as `2016-09-14T14:01:13.000000Z`.
323  :type subsecond: bool
324  :returns: A time string represented in ISO 8601 format.
325  :rtype: str
326  """
327  if not at:
328  at = timeutils.utcnow()
329  # NOTE(lbragstad): Datetime objects are immutable, so reassign the date we
330  # are working with to itself as we drop microsecond precision.
331  at = at.replace(microsecond=0)
332  st = at.strftime(_ISO8601_TIME_FORMAT
333  if not subsecond
334  else _ISO8601_TIME_FORMAT_SUBSECOND)
335  tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC'
336  # Need to handle either iso8601 or python UTC format
337  st += ('Z' if tz in ['UTC', 'UTC+00:00'] else tz)
338  return st
339 
340 
341 def parse_expiration_date(expiration_date):
342  if not expiration_date.endswith('Z'):
343  expiration_date += 'Z'
344  try:
345  expiration_time = timeutils.parse_isotime(expiration_date)
346  except ValueError:
348  if timeutils.is_older_than(expiration_time, 0):
350  return expiration_time
351 
352 
353 URL_RESERVED_CHARS = ":/?#[]@!$&'()*+,;="
354 
355 
356 def is_not_url_safe(name):
357  """Check if a string contains any url reserved characters."""
358  return len(list_url_unsafe_chars(name)) > 0
359 
360 
362  """Return a list of the reserved characters."""
363  reserved_chars = ''
364  for i in name:
365  if i in URL_RESERVED_CHARS:
366  reserved_chars += i
367  return reserved_chars
368 
369 
371  """Change the URL's hostname to lowercase."""
372  # NOTE(gyee): according to
373  # https://www.w3.org/TR/WD-html40-970708/htmlweb.html, the netloc portion
374  # of the URL is case-insensitive
375  parsed = urllib.parse.urlparse(url)
376  # Note: _replace method for named tuples is public and defined in docs
377  replaced = parsed._replace(netloc=parsed.netloc.lower())
378  return urllib.parse.urlunparse(replaced)
379 
380 
382  # remove the default ports specified in RFC2616 and 2818
383  o = urllib.parse.urlparse(url)
384  separator = ':'
385  (host, separator, port) = o.netloc.partition(separator)
386  if o.scheme.lower() == 'http' and port == '80':
387  # NOTE(gyee): _replace() is not a private method. It has
388  # an underscore prefix to prevent conflict with field names.
389  # See https://docs.python.org/2/library/collections.html#
390  # collections.namedtuple
391  o = o._replace(netloc=host)
392  if o.scheme.lower() == 'https' and port == '443':
393  o = o._replace(netloc=host)
394 
395  return urllib.parse.urlunparse(o)
396 
397 
398 def format_url(url, substitutions, silent_keyerror_failures=None):
399  """Format a user-defined URL with the given substitutions.
400 
401  :param string url: the URL to be formatted
402  :param dict substitutions: the dictionary used for substitution
403  :param list silent_keyerror_failures: keys for which we should be silent
404  if there is a KeyError exception on substitution attempt
405  :returns: a formatted URL
406 
407  """
408  substitutions = WhiteListedItemFilter(
409  WHITELISTED_PROPERTIES,
410  substitutions)
411  allow_keyerror = silent_keyerror_failures or []
412  try:
413  result = url.replace('$(', '%(') % substitutions
414  except AttributeError:
415  msg = "Malformed endpoint - %(url)r is not a string"
416  LOG.error(msg, {"url": url})
417  raise exception.MalformedEndpoint(endpoint=url)
418  except KeyError as e:
419  if not e.args or e.args[0] not in allow_keyerror:
420  msg = "Malformed endpoint %(url)s - unknown key %(keyerror)s"
421  LOG.error(msg, {"url": url, "keyerror": e})
422  raise exception.MalformedEndpoint(endpoint=url)
423  else:
424  result = None
425  except TypeError as e:
426  msg = ("Malformed endpoint '%(url)s'. The following type error "
427  "occurred during string substitution: %(typeerror)s")
428  LOG.error(msg, {"url": url, "typeerror": e})
429  raise exception.MalformedEndpoint(endpoint=url)
430  except ValueError:
431  msg = ("Malformed endpoint %s - incomplete format "
432  "(are you missing a type notifier ?)")
433  LOG.error(msg, url)
434  raise exception.MalformedEndpoint(endpoint=url)
435  return result
436 
437 
439  """Check substitution of url.
440 
441  The invalid urls are as follows:
442  urls with substitutions that is not in the whitelist
443 
444  Check the substitutions in the URL to make sure they are valid
445  and on the whitelist.
446 
447  :param str url: the URL to validate
448  :rtype: None
449  :raises keystone.exception.URLValidationError: if the URL is invalid
450  """
451  # check whether the property in the path is exactly the same
452  # with that in the whitelist below
453  substitutions = dict(zip(WHITELISTED_PROPERTIES, itertools.repeat('')))
454  try:
455  url.replace('$(', '%(') % substitutions
456  except (KeyError, TypeError, ValueError):
457  raise exception.URLValidationError(url=url)
458 
459 
460 def create_directory(directory, keystone_user_id=None, keystone_group_id=None):
461  """Attempt to create a directory if it doesn't exist.
462 
463  :param directory: string containing the path of the directory to create.
464  :param keystone_user_id: the system ID of the process running keystone.
465  :param keystone_group_id: the system ID of the group running keystone.
466 
467  """
468  if not os.access(directory, os.F_OK):
469  LOG.info(
470  '%s does not appear to exist; attempting to create it', directory
471  )
472 
473  try:
474  os.makedirs(directory, 0o700)
475  except OSError:
476  LOG.error(
477  'Failed to create %s: either it already '
478  'exists or you don\'t have sufficient permissions to '
479  'create it', directory
480  )
481 
482  if keystone_user_id and keystone_group_id:
483  os.chown(
484  directory,
485  keystone_user_id,
486  keystone_group_id)
487  elif keystone_user_id or keystone_group_id:
488  LOG.warning(
489  'Unable to change the ownership of key repository without '
490  'a keystone user ID and keystone group ID both being '
491  'provided: %s', directory)
keystone.common.utils.get_unix_group
def get_unix_group(group=None)
Definition: utils.py:222
keystone.common.utils.is_not_url_safe
def is_not_url_safe(name)
Definition: utils.py:356
keystone.common.utils.lower_case_hostname
def lower_case_hostname(url)
Definition: utils.py:370
keystone.common.utils.setup_remote_pydev_debug
def setup_remote_pydev_debug()
Definition: utils.py:144
keystone.exception.URLValidationError
Definition: exception.py:107
keystone.common.utils.format_url
def format_url(url, substitutions, silent_keyerror_failures=None)
Definition: utils.py:398
keystone.common.utils.isotime
def isotime(at=None, subsecond=False)
Definition: utils.py:299
keystone.common.utils.get_unix_user
def get_unix_user(user=None)
Definition: utils.py:165
keystone.common.utils.SmarterEncoder.default
def default(self, obj)
Definition: utils.py:94
keystone.common.utils.create_directory
def create_directory(directory, keystone_user_id=None, keystone_group_id=None)
Definition: utils.py:460
keystone.common.utils.WhiteListedItemFilter.__init__
def __init__(self, whitelist, data)
Definition: utils.py:284
keystone.common.utils.resource_uuid
def resource_uuid(value)
Definition: utils.py:62
keystone.exception.ValidationTimeStampError
Definition: exception.py:150
keystone.common.utils.WhiteListedItemFilter._whitelist
_whitelist
Definition: utils.py:285
keystone.exception.MalformedEndpoint
Definition: exception.py:594
keystone.common.utils.WhiteListedItemFilter
Definition: utils.py:282
keystone.common.utils.attr_as_boolean
def attr_as_boolean(val_attr)
Definition: utils.py:108
keystone.common.utils.hash_access_key
def hash_access_key(access)
Definition: utils.py:100
keystone.common.utils.WhiteListedItemFilter._data
_data
Definition: utils.py:286
keystone.common.utils.flatten_dict
def flatten_dict(d, parent_key='')
Definition: utils.py:74
keystone.exception.ValidationExpirationError
Definition: exception.py:165
keystone.conf
Definition: __init__.py:1
keystone.i18n._
_
Definition: i18n.py:29
keystone.common.utils.auth_str_equal
def auth_str_equal(provided, known)
Definition: utils.py:120
keystone.common
Definition: __init__.py:1
keystone.i18n
Definition: i18n.py:1
keystone.common.utils.WhiteListedItemFilter.__getitem__
def __getitem__(self, name)
Definition: utils.py:288
keystone.common.utils.SmarterEncoder
Definition: utils.py:91
keystone.common.utils.list_url_unsafe_chars
def list_url_unsafe_chars(name)
Definition: utils.py:361
keystone.common.utils.parse_expiration_date
def parse_expiration_date(expiration_date)
Definition: utils.py:341
keystone.common.utils.check_endpoint_url
def check_endpoint_url(url)
Definition: utils.py:438
keystone.common.utils.remove_standard_port
def remove_standard_port(url)
Definition: utils.py:381