OpenStack Context library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

context.py 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. # Copyright 2011 OpenStack Foundation.
  2. # All Rights Reserved.
  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. Base class for holding contextual information of a request
  17. This class has several uses:
  18. * Used for storing security information in a web request.
  19. * Used for passing contextual details to oslo.log.
  20. Projects should subclass this class if they wish to enhance the request
  21. context or provide additional information in their specific WSGI pipeline
  22. or logging context.
  23. """
  24. import collections
  25. import functools
  26. import itertools
  27. import threading
  28. import uuid
  29. import warnings
  30. import debtcollector
  31. from debtcollector import renames
  32. _request_store = threading.local()
  33. # These arguments will be passed to a new context from the first available
  34. # header to support backwards compatibility.
  35. _ENVIRON_HEADERS = {
  36. 'auth_token': ['HTTP_X_AUTH_TOKEN',
  37. 'HTTP_X_STORAGE_TOKEN'],
  38. 'user_id': ['HTTP_X_USER_ID',
  39. 'HTTP_X_USER'],
  40. 'project_id': ['HTTP_X_PROJECT_ID',
  41. 'HTTP_X_TENANT_ID',
  42. 'HTTP_X_TENANT'],
  43. 'domain_id': ['HTTP_X_DOMAIN_ID'],
  44. 'system_scope': ['HTTP_OPENSTACK_SYSTEM_SCOPE'],
  45. 'user_domain_id': ['HTTP_X_USER_DOMAIN_ID'],
  46. 'project_domain_id': ['HTTP_X_PROJECT_DOMAIN_ID'],
  47. 'user_name': ['HTTP_X_USER_NAME'],
  48. 'project_name': ['HTTP_X_PROJECT_NAME',
  49. 'HTTP_X_TENANT_NAME'],
  50. 'user_domain_name': ['HTTP_X_USER_DOMAIN_NAME'],
  51. 'project_domain_name': ['HTTP_X_PROJECT_DOMAIN_NAME'],
  52. 'request_id': ['openstack.request_id'],
  53. 'global_request_id': ['openstack.global_request_id'],
  54. 'service_token': ['HTTP_X_SERVICE_TOKEN'],
  55. 'service_user_id': ['HTTP_X_SERVICE_USER_ID'],
  56. 'service_user_name': ['HTTP_X_SERVICE_USER_NAME'],
  57. 'service_user_domain_id': ['HTTP_X_SERVICE_USER_DOMAIN_ID'],
  58. 'service_user_domain_name': ['HTTP_X_SERVICE_USER_DOMAIN_NAME'],
  59. 'service_project_id': ['HTTP_X_SERVICE_PROJECT_ID'],
  60. 'service_project_name': ['HTTP_X_SERVICE_PROJECT_NAME'],
  61. 'service_project_domain_id': ['HTTP_X_SERVICE_PROJECT_DOMAIN_ID'],
  62. 'service_project_domain_name': ['HTTP_X_SERVICE_PROJECT_DOMAIN_NAME'],
  63. }
  64. def generate_request_id():
  65. """Generate a unique request id."""
  66. return 'req-%s' % uuid.uuid4()
  67. class _DeprecatedPolicyValues(collections.MutableMapping):
  68. """A Dictionary that manages current and deprecated policy values.
  69. Anything added to this dictionary after initial creation is considered a
  70. deprecated key that we are trying to move services away from. Accessing
  71. these values as oslo.policy will do will trigger a DeprecationWarning.
  72. """
  73. def __init__(self, data):
  74. self._data = data
  75. self._deprecated = {}
  76. def __getitem__(self, k):
  77. try:
  78. return self._data[k]
  79. except KeyError:
  80. pass
  81. try:
  82. val = self._deprecated[k]
  83. except KeyError:
  84. pass
  85. else:
  86. warnings.warn('Policy enforcement is depending on the value of '
  87. '%s. This key is deprecated. Please update your '
  88. 'policy file to use the standard policy values.' % k,
  89. DeprecationWarning)
  90. return val
  91. raise KeyError(k)
  92. def __setitem__(self, k, v):
  93. self._deprecated[k] = v
  94. def __delitem__(self, k):
  95. del self._deprecated[k]
  96. def __iter__(self):
  97. return iter(self._dict)
  98. def __len__(self):
  99. return len(self._dict)
  100. def __str__(self):
  101. return self._dict.__str__()
  102. def __repr__(self):
  103. return self._dict.__repr__()
  104. @property
  105. def _dict(self):
  106. d = self._deprecated.copy()
  107. d.update(self._data)
  108. return d
  109. def _moved_msg(new_name, old_name):
  110. if old_name:
  111. deprecated_msg = "Property '%(old_name)s' has moved to '%(new_name)s'"
  112. deprecated_msg = deprecated_msg % {'old_name': old_name,
  113. 'new_name': new_name}
  114. debtcollector.deprecate(deprecated_msg,
  115. version='2.6',
  116. removal_version='3.0',
  117. stacklevel=5)
  118. def _moved_property(new_name, old_name=None, target=None):
  119. if not target:
  120. target = new_name
  121. def getter(self):
  122. _moved_msg(new_name, old_name)
  123. return getattr(self, target)
  124. def setter(self, value):
  125. _moved_msg(new_name, old_name)
  126. setattr(self, target, value)
  127. def deleter(self):
  128. _moved_msg(new_name, old_name)
  129. delattr(self, target)
  130. return property(getter, setter, deleter)
  131. _renamed_kwarg = functools.partial(renames.renamed_kwarg,
  132. version='2.18',
  133. removal_version='3.0',
  134. replace=True)
  135. class RequestContext(object):
  136. """Helper class to represent useful information about a request context.
  137. Stores information about the security context under which the user
  138. accesses the system, as well as additional request information.
  139. """
  140. user_idt_format = u'{user} {tenant} {domain} {user_domain} {p_domain}'
  141. # Can be overridden in subclasses to specify extra keys that should be
  142. # read when constructing a context using from_dict.
  143. FROM_DICT_EXTRA_KEYS = []
  144. @_renamed_kwarg('user', 'user_id')
  145. @_renamed_kwarg('tenant', 'project_id')
  146. @_renamed_kwarg('domain', 'domain_id')
  147. @_renamed_kwarg('user_domain', 'user_domain_id')
  148. @_renamed_kwarg('project_domain', 'project_domain_id')
  149. def __init__(self,
  150. auth_token=None,
  151. user_id=None,
  152. project_id=None,
  153. domain_id=None,
  154. user_domain_id=None,
  155. project_domain_id=None,
  156. is_admin=False,
  157. read_only=False,
  158. show_deleted=False,
  159. request_id=None,
  160. resource_uuid=None,
  161. overwrite=True,
  162. roles=None,
  163. user_name=None,
  164. project_name=None,
  165. domain_name=None,
  166. user_domain_name=None,
  167. project_domain_name=None,
  168. is_admin_project=True,
  169. service_token=None,
  170. service_user_id=None,
  171. service_user_name=None,
  172. service_user_domain_id=None,
  173. service_user_domain_name=None,
  174. service_project_id=None,
  175. service_project_name=None,
  176. service_project_domain_id=None,
  177. service_project_domain_name=None,
  178. service_roles=None,
  179. global_request_id=None,
  180. system_scope=None):
  181. """Initialize the RequestContext
  182. :param overwrite: Set to False to ensure that the greenthread local
  183. copy of the index is not overwritten.
  184. :param is_admin_project: Whether the specified project is specified in
  185. the token as the admin project. Defaults to
  186. True for backwards compatibility.
  187. :type is_admin_project: bool
  188. :param system_scope: The system scope of a token. The value ``all``
  189. represents the entire deployment system. A service
  190. ID represents a specific service within the
  191. deployment system.
  192. :type system_scope: string
  193. """
  194. # setting to private variables to avoid triggering subclass properties
  195. self._user_id = user_id
  196. self._project_id = project_id
  197. self._domain_id = domain_id
  198. self._user_domain_id = user_domain_id
  199. self._project_domain_id = project_domain_id
  200. self.auth_token = auth_token
  201. self.user_name = user_name
  202. self.project_name = project_name
  203. self.domain_name = domain_name
  204. self.system_scope = system_scope
  205. self.user_domain_name = user_domain_name
  206. self.project_domain_name = project_domain_name
  207. self.is_admin = is_admin
  208. self.is_admin_project = is_admin_project
  209. self.read_only = read_only
  210. self.show_deleted = show_deleted
  211. self.resource_uuid = resource_uuid
  212. self.roles = roles or []
  213. self.service_token = service_token
  214. self.service_user_id = service_user_id
  215. self.service_user_name = service_user_name
  216. self.service_user_domain_id = service_user_domain_id
  217. self.service_user_domain_name = service_user_domain_name
  218. self.service_project_id = service_project_id
  219. self.service_project_name = service_project_name
  220. self.service_project_domain_id = service_project_domain_id
  221. self.service_project_domain_name = service_project_domain_name
  222. self.service_roles = service_roles or []
  223. if not request_id:
  224. request_id = generate_request_id()
  225. self.request_id = request_id
  226. self.global_request_id = global_request_id
  227. if overwrite or not get_current():
  228. self.update_store()
  229. # NOTE(jamielennox): To prevent circular lookups on subclasses that might
  230. # point user to user_id we make user/user_id tenant/project_id etc point
  231. # to the same private variable rather than each other.
  232. tenant = _moved_property('project_id', 'tenant', target='_project_id')
  233. user = _moved_property('user_id', 'user', target='_user_id')
  234. domain = _moved_property('domain_id', 'domain', target='_domain_id')
  235. user_domain = _moved_property('user_domain_id',
  236. 'user_domain',
  237. target='_user_domain_id')
  238. project_domain = _moved_property('project_domain_id',
  239. 'project_domain',
  240. target='_project_domain_id')
  241. user_id = _moved_property('_user_id')
  242. project_id = _moved_property('_project_id')
  243. domain_id = _moved_property('_domain_id')
  244. user_domain_id = _moved_property('_user_domain_id')
  245. project_domain_id = _moved_property('_project_domain_id')
  246. def update_store(self):
  247. """Store the context in the current thread."""
  248. _request_store.context = self
  249. def to_policy_values(self):
  250. """A dictionary of context attributes to enforce policy with.
  251. oslo.policy enforcement requires a dictionary of attributes
  252. representing the current logged in user on which it applies policy
  253. enforcement. This dictionary defines a standard list of attributes that
  254. should be available for enforcement across services.
  255. It is expected that services will often have to override this method
  256. with either deprecated values or additional attributes used by that
  257. service specific policy.
  258. """
  259. # NOTE(jamielennox): We need a way to allow projects to provide old
  260. # deprecated policy values that trigger a warning when used in favour
  261. # of our standard ones. This object acts like a dict but only values
  262. # from oslo.policy don't show a warning.
  263. return _DeprecatedPolicyValues({
  264. 'user_id': self.user_id,
  265. 'user_domain_id': self.user_domain_id,
  266. 'system_scope': self.system_scope,
  267. 'domain_id': self.domain_id,
  268. 'project_id': self.project_id,
  269. 'project_domain_id': self.project_domain_id,
  270. 'roles': self.roles,
  271. 'is_admin_project': self.is_admin_project,
  272. 'service_user_id': self.service_user_id,
  273. 'service_user_domain_id': self.service_user_domain_id,
  274. 'service_project_id': self.service_project_id,
  275. 'service_project_domain_id': self.service_project_domain_id,
  276. 'service_roles': self.service_roles})
  277. def to_dict(self):
  278. """Return a dictionary of context attributes."""
  279. user_idt = self.user_idt_format.format(
  280. user=self.user_id or '-',
  281. tenant=self.project_id or '-',
  282. domain=self.domain_id or '-',
  283. user_domain=self.user_domain_id or '-',
  284. p_domain=self.project_domain_id or '-')
  285. return {'user': self.user_id,
  286. 'tenant': self.project_id,
  287. 'system_scope': self.system_scope,
  288. 'project': self.project_id,
  289. 'domain': self.domain_id,
  290. 'user_domain': self.user_domain_id,
  291. 'project_domain': self.project_domain_id,
  292. 'is_admin': self.is_admin,
  293. 'read_only': self.read_only,
  294. 'show_deleted': self.show_deleted,
  295. 'auth_token': self.auth_token,
  296. 'request_id': self.request_id,
  297. 'global_request_id': self.global_request_id,
  298. 'resource_uuid': self.resource_uuid,
  299. 'roles': self.roles,
  300. 'user_identity': user_idt,
  301. 'is_admin_project': self.is_admin_project}
  302. def get_logging_values(self):
  303. """Return a dictionary of logging specific context attributes."""
  304. values = {'user_name': self.user_name,
  305. 'project_name': self.project_name,
  306. 'domain_name': self.domain_name,
  307. 'user_domain_name': self.user_domain_name,
  308. 'project_domain_name': self.project_domain_name}
  309. values.update(self.to_dict())
  310. if self.auth_token:
  311. # NOTE(jaosorior): Gotta obfuscate the token since this dict is
  312. # meant for logging and we shouldn't leak it.
  313. values['auth_token'] = '***'
  314. else:
  315. values['auth_token'] = None
  316. return values
  317. @property
  318. def global_id(self):
  319. """Return a sensible value for global_id to pass on.
  320. When we want to make a call with to another service, it's
  321. important that we try to use global_request_id if available,
  322. and fall back to the locally generated request_id if not.
  323. """
  324. return self.global_request_id or self.request_id
  325. @classmethod
  326. @_renamed_kwarg('user', 'user_id')
  327. @_renamed_kwarg('tenant', 'project_id')
  328. @_renamed_kwarg('domain', 'domain_id')
  329. @_renamed_kwarg('user_domain', 'user_domain_id')
  330. @_renamed_kwarg('project_domain', 'project_domain_id')
  331. def from_dict(cls, values, **kwargs):
  332. """Construct a context object from a provided dictionary."""
  333. kwargs.setdefault('auth_token', values.get('auth_token'))
  334. kwargs.setdefault('user_id', values.get('user'))
  335. kwargs.setdefault('project_id', values.get('tenant'))
  336. kwargs.setdefault('domain_id', values.get('domain'))
  337. kwargs.setdefault('user_domain_id', values.get('user_domain'))
  338. kwargs.setdefault('project_domain_id', values.get('project_domain'))
  339. kwargs.setdefault('is_admin', values.get('is_admin', False))
  340. kwargs.setdefault('read_only', values.get('read_only', False))
  341. kwargs.setdefault('show_deleted', values.get('show_deleted', False))
  342. kwargs.setdefault('request_id', values.get('request_id'))
  343. kwargs.setdefault('global_request_id', values.get('global_request_id'))
  344. kwargs.setdefault('resource_uuid', values.get('resource_uuid'))
  345. kwargs.setdefault('roles', values.get('roles'))
  346. kwargs.setdefault('user_name', values.get('user_name'))
  347. kwargs.setdefault('project_name', values.get('project_name'))
  348. kwargs.setdefault('domain_name', values.get('domain_name'))
  349. kwargs.setdefault('user_domain_name', values.get('user_domain_name'))
  350. kwargs.setdefault('project_domain_name',
  351. values.get('project_domain_name'))
  352. kwargs.setdefault('is_admin_project',
  353. values.get('is_admin_project', True))
  354. for key in cls.FROM_DICT_EXTRA_KEYS:
  355. kwargs.setdefault(key, values.get(key))
  356. return cls(**kwargs)
  357. @classmethod
  358. @_renamed_kwarg('user', 'user_id')
  359. @_renamed_kwarg('tenant', 'project_id')
  360. @_renamed_kwarg('domain', 'domain_id')
  361. @_renamed_kwarg('user_domain', 'user_domain_id')
  362. @_renamed_kwarg('project_domain', 'project_domain_id')
  363. def from_environ(cls, environ, **kwargs):
  364. """Load a context object from a request environment.
  365. If keyword arguments are provided then they override the values in the
  366. request environment.
  367. :param environ: The environment dictionary associated with a request.
  368. :type environ: dict
  369. """
  370. # Load a new context object from the environment variables set by
  371. # auth_token middleware. See:
  372. # https://docs.openstack.org/keystonemiddleware/latest/api/keystonemiddleware.auth_token.html#what-auth-token-adds-to-the-request-for-use-by-the-openstack-service
  373. # add kwarg if not specified by user from a list of possible headers
  374. for k, v_list in _ENVIRON_HEADERS.items():
  375. if k in kwargs:
  376. continue
  377. for v in v_list:
  378. if v in environ:
  379. kwargs[k] = environ[v]
  380. break
  381. if 'roles' not in kwargs:
  382. roles = environ.get('HTTP_X_ROLES', environ.get('HTTP_X_ROLE'))
  383. roles = [r.strip() for r in roles.split(',')] if roles else []
  384. kwargs['roles'] = roles
  385. if 'service_roles' not in kwargs:
  386. roles = environ.get('HTTP_X_SERVICE_ROLES')
  387. roles = [r.strip() for r in roles.split(',')] if roles else []
  388. kwargs['service_roles'] = roles
  389. if 'is_admin_project' not in kwargs:
  390. # NOTE(jamielennox): we default is_admin_project to true because if
  391. # nothing is provided we have to assume it is the admin project to
  392. # make old policy continue to work.
  393. is_admin_proj_str = environ.get('HTTP_X_IS_ADMIN_PROJECT', 'true')
  394. kwargs['is_admin_project'] = is_admin_proj_str.lower() == 'true'
  395. return cls(**kwargs)
  396. def get_admin_context(show_deleted=False):
  397. """Create an administrator context."""
  398. context = RequestContext(None,
  399. project_id=None,
  400. is_admin=True,
  401. show_deleted=show_deleted,
  402. overwrite=False)
  403. return context
  404. def get_context_from_function_and_args(function, args, kwargs):
  405. """Find an arg of type RequestContext and return it.
  406. This is useful in a couple of decorators where we don't
  407. know much about the function we're wrapping.
  408. """
  409. for arg in itertools.chain(kwargs.values(), args):
  410. if isinstance(arg, RequestContext):
  411. return arg
  412. return None
  413. def is_user_context(context):
  414. """Indicates if the request context is a normal user."""
  415. if not context or not isinstance(context, RequestContext):
  416. return False
  417. if context.is_admin:
  418. return False
  419. return True
  420. def get_current():
  421. """Return this thread's current context
  422. If no context is set, returns None
  423. """
  424. return getattr(_request_store, 'context', None)