OpenStack Identity (Keystone)
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.
 
 
 
 

845 lines
32 KiB

  1. # Copyright 2013 IBM Corp.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  4. # not use this file except in compliance with the License. You may obtain
  5. # a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  11. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  12. # License for the specific language governing permissions and limitations
  13. # under the License.
  14. """Notifications module for OpenStack Identity Service resources."""
  15. import collections
  16. import functools
  17. import inspect
  18. import socket
  19. import flask
  20. from oslo_log import log
  21. import oslo_messaging
  22. from oslo_utils import reflection
  23. import pycadf
  24. from pycadf import cadftaxonomy as taxonomy
  25. from pycadf import cadftype
  26. from pycadf import credential
  27. from pycadf import eventfactory
  28. from pycadf import host
  29. from pycadf import reason
  30. from pycadf import resource
  31. from keystone.common import context
  32. from keystone.common import provider_api
  33. from keystone.common import utils
  34. import keystone.conf
  35. from keystone import exception
  36. from keystone.i18n import _
  37. _CATALOG_HELPER_OBJ = None
  38. LOG = log.getLogger(__name__)
  39. # NOTE(gyee): actions that can be notified. One must update this list whenever
  40. # a new action is supported.
  41. _ACTIONS = collections.namedtuple(
  42. 'NotificationActions',
  43. 'created, deleted, disabled, updated, internal')
  44. ACTIONS = _ACTIONS(created='created', deleted='deleted', disabled='disabled',
  45. updated='updated', internal='internal')
  46. """The actions on resources."""
  47. CADF_TYPE_MAP = {
  48. 'group': taxonomy.SECURITY_GROUP,
  49. 'project': taxonomy.SECURITY_PROJECT,
  50. 'role': taxonomy.SECURITY_ROLE,
  51. 'user': taxonomy.SECURITY_ACCOUNT_USER,
  52. 'domain': taxonomy.SECURITY_DOMAIN,
  53. 'region': taxonomy.SECURITY_REGION,
  54. 'endpoint': taxonomy.SECURITY_ENDPOINT,
  55. 'service': taxonomy.SECURITY_SERVICE,
  56. 'policy': taxonomy.SECURITY_POLICY,
  57. 'OS-TRUST:trust': taxonomy.SECURITY_TRUST,
  58. 'OS-OAUTH1:access_token': taxonomy.SECURITY_CREDENTIAL,
  59. 'OS-OAUTH1:request_token': taxonomy.SECURITY_CREDENTIAL,
  60. 'OS-OAUTH1:consumer': taxonomy.SECURITY_ACCOUNT,
  61. 'application_credential': taxonomy.SECURITY_CREDENTIAL,
  62. }
  63. SAML_AUDIT_TYPE = 'http://docs.oasis-open.org/security/saml/v2.0'
  64. # resource types that can be notified
  65. _SUBSCRIBERS = {}
  66. _notifier = None
  67. SERVICE = 'identity'
  68. PROVIDERS = provider_api.ProviderAPIs
  69. ROOT_DOMAIN = '<<keystone.domain.root>>'
  70. CONF = keystone.conf.CONF
  71. # NOTE(morganfainberg): Special case notifications that are only used
  72. # internally for handling token persistence token deletions
  73. INVALIDATE_TOKEN_CACHE = 'invalidate_token_cache' # nosec
  74. PERSIST_REVOCATION_EVENT_FOR_USER = 'persist_revocation_event_for_user'
  75. REMOVE_APP_CREDS_FOR_USER = 'remove_application_credentials_for_user'
  76. DOMAIN_DELETED = 'domain_deleted'
  77. def build_audit_initiator():
  78. """A pyCADF initiator describing the current authenticated context."""
  79. pycadf_host = host.Host(address=flask.request.remote_addr,
  80. agent=str(flask.request.user_agent))
  81. initiator = resource.Resource(typeURI=taxonomy.ACCOUNT_USER,
  82. host=pycadf_host)
  83. oslo_context = flask.request.environ.get(context.REQUEST_CONTEXT_ENV)
  84. if oslo_context.user_id:
  85. initiator.id = utils.resource_uuid(oslo_context.user_id)
  86. initiator.user_id = oslo_context.user_id
  87. if oslo_context.project_id:
  88. initiator.project_id = oslo_context.project_id
  89. if oslo_context.domain_id:
  90. initiator.domain_id = oslo_context.domain_id
  91. initiator.request_id = oslo_context.request_id
  92. if oslo_context.global_request_id:
  93. initiator.global_request_id = oslo_context.global_request_id
  94. return initiator
  95. class Audit(object):
  96. """Namespace for audit notification functions.
  97. This is a namespace object to contain all of the direct notification
  98. functions utilized for ``Manager`` methods.
  99. """
  100. @classmethod
  101. def _emit(cls, operation, resource_type, resource_id, initiator, public,
  102. actor_dict=None, reason=None):
  103. """Directly send an event notification.
  104. :param operation: one of the values from ACTIONS
  105. :param resource_type: type of resource being affected
  106. :param resource_id: ID of the resource affected
  107. :param initiator: CADF representation of the user that created the
  108. request
  109. :param public: If True (default), the event will be sent to the
  110. notifier API. If False, the event will only be sent via
  111. notify_event_callbacks to in process listeners
  112. :param actor_dict: dictionary of actor information in the event of
  113. assignment notification
  114. :param reason: pycadf object containing the response code and
  115. message description
  116. """
  117. # NOTE(stevemar): the _send_notification function is
  118. # overloaded, it's used to register callbacks and to actually
  119. # send the notification externally. Thus, we should check
  120. # the desired notification format in the function instead
  121. # of before it.
  122. _send_notification(
  123. operation,
  124. resource_type,
  125. resource_id,
  126. initiator=initiator,
  127. actor_dict=actor_dict,
  128. public=public)
  129. if CONF.notification_format == 'cadf' and public:
  130. outcome = taxonomy.OUTCOME_SUCCESS
  131. _create_cadf_payload(operation, resource_type, resource_id,
  132. outcome, initiator, reason)
  133. @classmethod
  134. def created(cls, resource_type, resource_id, initiator=None,
  135. public=True, reason=None):
  136. cls._emit(ACTIONS.created, resource_type, resource_id, initiator,
  137. public, reason=reason)
  138. @classmethod
  139. def updated(cls, resource_type, resource_id, initiator=None,
  140. public=True, reason=None):
  141. cls._emit(ACTIONS.updated, resource_type, resource_id, initiator,
  142. public, reason=reason)
  143. @classmethod
  144. def disabled(cls, resource_type, resource_id, initiator=None,
  145. public=True, reason=None):
  146. cls._emit(ACTIONS.disabled, resource_type, resource_id, initiator,
  147. public, reason=reason)
  148. @classmethod
  149. def deleted(cls, resource_type, resource_id, initiator=None,
  150. public=True, reason=None):
  151. cls._emit(ACTIONS.deleted, resource_type, resource_id, initiator,
  152. public, reason=reason)
  153. @classmethod
  154. def added_to(cls, target_type, target_id, actor_type, actor_id,
  155. initiator=None, public=True, reason=None):
  156. actor_dict = {'id': actor_id,
  157. 'type': actor_type,
  158. 'actor_operation': 'added'}
  159. cls._emit(ACTIONS.updated, target_type, target_id, initiator, public,
  160. actor_dict=actor_dict, reason=reason)
  161. @classmethod
  162. def removed_from(cls, target_type, target_id, actor_type, actor_id,
  163. initiator=None, public=True, reason=None):
  164. actor_dict = {'id': actor_id,
  165. 'type': actor_type,
  166. 'actor_operation': 'removed'}
  167. cls._emit(ACTIONS.updated, target_type, target_id, initiator, public,
  168. actor_dict=actor_dict, reason=reason)
  169. @classmethod
  170. def internal(cls, resource_type, resource_id, reason=None):
  171. # NOTE(lbragstad): Internal notifications are never public and have
  172. # never used the initiator variable, but the _emit() method expects
  173. # them. Let's set them here but not expose them through the method
  174. # signature - that way someone can not do something like send an
  175. # internal notification publicly.
  176. initiator = None
  177. public = False
  178. cls._emit(ACTIONS.internal, resource_type, resource_id, initiator,
  179. public, reason)
  180. def invalidate_token_cache_notification(reason):
  181. """A specific notification for invalidating the token cache.
  182. :param reason: The specific reason why the token cache is being
  183. invalidated.
  184. :type reason: string
  185. """
  186. # Since keystone does a lot of work in the authentication and validation
  187. # process to make sure the authorization context for the user is
  188. # update-to-date, invalidating the token cache is a somewhat common
  189. # operation. It's done across various subsystems when role assignments
  190. # change, users are disabled, identity providers deleted or disabled, etc..
  191. # This notification is meant to make the process of invalidating the token
  192. # cache DRY, instead of have each subsystem implement their own token cache
  193. # invalidation strategy or callbacks.
  194. LOG.debug(reason)
  195. resource_id = None
  196. initiator = None
  197. public = False
  198. Audit._emit(
  199. ACTIONS.internal, INVALIDATE_TOKEN_CACHE, resource_id, initiator,
  200. public, reason=reason
  201. )
  202. def _get_callback_info(callback):
  203. """Return list containing callback's module and name.
  204. If the callback is a bound instance method also return the class name.
  205. :param callback: Function to call
  206. :type callback: function
  207. :returns: List containing parent module, (optional class,) function name
  208. :rtype: list
  209. """
  210. module_name = getattr(callback, '__module__', None)
  211. func_name = callback.__name__
  212. if inspect.ismethod(callback):
  213. class_name = reflection.get_class_name(callback.__self__,
  214. fully_qualified=False)
  215. return [module_name, class_name, func_name]
  216. else:
  217. return [module_name, func_name]
  218. def register_event_callback(event, resource_type, callbacks):
  219. """Register each callback with the event.
  220. :param event: Action being registered
  221. :type event: keystone.notifications.ACTIONS
  222. :param resource_type: Type of resource being operated on
  223. :type resource_type: str
  224. :param callbacks: Callback items to be registered with event
  225. :type callbacks: list
  226. :raises ValueError: If event is not a valid ACTION
  227. :raises TypeError: If callback is not callable
  228. """
  229. if event not in ACTIONS:
  230. raise ValueError(_('%(event)s is not a valid notification event, must '
  231. 'be one of: %(actions)s') %
  232. {'event': event, 'actions': ', '.join(ACTIONS)})
  233. if not hasattr(callbacks, '__iter__'):
  234. callbacks = [callbacks]
  235. for callback in callbacks:
  236. if not callable(callback):
  237. msg = 'Method not callable: %s' % callback
  238. tr_msg = _('Method not callable: %s') % callback
  239. LOG.error(msg)
  240. raise TypeError(tr_msg)
  241. _SUBSCRIBERS.setdefault(event, {}).setdefault(resource_type, set())
  242. _SUBSCRIBERS[event][resource_type].add(callback)
  243. if LOG.logger.getEffectiveLevel() <= log.DEBUG:
  244. # Do this only if its going to appear in the logs.
  245. msg = 'Callback: `%(callback)s` subscribed to event `%(event)s`.'
  246. callback_info = _get_callback_info(callback)
  247. callback_str = '.'.join(i for i in callback_info if i is not None)
  248. event_str = '.'.join(['identity', resource_type, event])
  249. LOG.debug(msg, {'callback': callback_str, 'event': event_str})
  250. def listener(cls):
  251. """A class decorator to declare a class to be a notification listener.
  252. A notification listener must specify the event(s) it is interested in by
  253. defining a ``event_callbacks`` attribute or property. ``event_callbacks``
  254. is a dictionary where the key is the type of event and the value is a
  255. dictionary containing a mapping of resource types to callback(s).
  256. :data:`.ACTIONS` contains constants for the currently
  257. supported events. There is currently no single place to find constants for
  258. the resource types.
  259. Example::
  260. @listener
  261. class Something(object):
  262. def __init__(self):
  263. self.event_callbacks = {
  264. notifications.ACTIONS.created: {
  265. 'user': self._user_created_callback,
  266. },
  267. notifications.ACTIONS.deleted: {
  268. 'project': [
  269. self._project_deleted_callback,
  270. self._do_cleanup,
  271. ]
  272. },
  273. }
  274. """
  275. def init_wrapper(init):
  276. @functools.wraps(init)
  277. def __new_init__(self, *args, **kwargs):
  278. init(self, *args, **kwargs)
  279. _register_event_callbacks(self)
  280. return __new_init__
  281. def _register_event_callbacks(self):
  282. for event, resource_types in self.event_callbacks.items():
  283. for resource_type, callbacks in resource_types.items():
  284. register_event_callback(event, resource_type, callbacks)
  285. cls.__init__ = init_wrapper(cls.__init__)
  286. return cls
  287. def notify_event_callbacks(service, resource_type, operation, payload):
  288. """Send a notification to registered extensions."""
  289. if operation in _SUBSCRIBERS:
  290. if resource_type in _SUBSCRIBERS[operation]:
  291. for cb in _SUBSCRIBERS[operation][resource_type]:
  292. subst_dict = {'cb_name': cb.__name__,
  293. 'service': service,
  294. 'resource_type': resource_type,
  295. 'operation': operation,
  296. 'payload': payload}
  297. LOG.debug('Invoking callback %(cb_name)s for event '
  298. '%(service)s %(resource_type)s %(operation)s for '
  299. '%(payload)s', subst_dict)
  300. cb(service, resource_type, operation, payload)
  301. def _get_notifier():
  302. """Return a notifier object.
  303. If _notifier is None it means that a notifier object has not been set.
  304. If _notifier is False it means that a notifier has previously failed to
  305. construct.
  306. Otherwise it is a constructed Notifier object.
  307. """
  308. global _notifier
  309. if _notifier is None:
  310. host = CONF.default_publisher_id or socket.gethostname()
  311. try:
  312. transport = oslo_messaging.get_notification_transport(CONF)
  313. _notifier = oslo_messaging.Notifier(transport,
  314. "identity.%s" % host)
  315. except Exception:
  316. LOG.exception("Failed to construct notifier")
  317. _notifier = False
  318. return _notifier
  319. def clear_subscribers():
  320. """Empty subscribers dictionary.
  321. This effectively stops notifications since there will be no subscribers
  322. to publish to.
  323. """
  324. _SUBSCRIBERS.clear()
  325. def reset_notifier():
  326. """Reset the notifications internal state.
  327. This is used only for testing purposes.
  328. """
  329. global _notifier
  330. _notifier = None
  331. def _create_cadf_payload(operation, resource_type, resource_id,
  332. outcome, initiator, reason=None):
  333. """Prepare data for CADF audit notifier.
  334. Transform the arguments into content to be consumed by the function that
  335. emits CADF events (_send_audit_notification). Specifically the
  336. ``resource_type`` (role, user, etc) must be transformed into a CADF
  337. keyword, such as: ``data/security/role``. The ``resource_id`` is added as a
  338. top level value for the ``resource_info`` key. Lastly, the ``operation`` is
  339. used to create the CADF ``action``, and the ``event_type`` name.
  340. As per the CADF specification, the ``action`` must start with create,
  341. update, delete, etc... i.e.: created.user or deleted.role
  342. However the ``event_type`` is an OpenStack-ism that is typically of the
  343. form project.resource.operation. i.e.: identity.project.updated
  344. :param operation: operation being performed (created, updated, or deleted)
  345. :param resource_type: type of resource being operated on (role, user, etc)
  346. :param resource_id: ID of resource being operated on
  347. :param outcome: outcomes of the operation (SUCCESS, FAILURE, etc)
  348. :param initiator: CADF representation of the user that created the request
  349. :param reason: pycadf object containing the response code and
  350. message description
  351. """
  352. if resource_type not in CADF_TYPE_MAP:
  353. target_uri = taxonomy.UNKNOWN
  354. else:
  355. target_uri = CADF_TYPE_MAP.get(resource_type)
  356. # TODO(gagehugo): The root domain ID is typically hidden, there isn't a
  357. # reason to emit a notification for it. Once we expose the root domain
  358. # (and handle the CADF UUID), remove this.
  359. if resource_id == ROOT_DOMAIN:
  360. return
  361. target = resource.Resource(typeURI=target_uri,
  362. id=resource_id)
  363. audit_kwargs = {'resource_info': resource_id}
  364. cadf_action = '%s.%s' % (operation, resource_type)
  365. event_type = '%s.%s.%s' % (SERVICE, resource_type, operation)
  366. _send_audit_notification(cadf_action, initiator, outcome,
  367. target, event_type, reason=reason, **audit_kwargs)
  368. def _send_notification(operation, resource_type, resource_id, initiator=None,
  369. actor_dict=None, public=True):
  370. """Send notification to inform observers about the affected resource.
  371. This method doesn't raise an exception when sending the notification fails.
  372. :param operation: operation being performed (created, updated, or deleted)
  373. :param resource_type: type of resource being operated on
  374. :param resource_id: ID of resource being operated on
  375. :param initiator: representation of the user that created the request
  376. :param actor_dict: a dictionary containing the actor's ID and type
  377. :param public: if True (default), the event will be sent
  378. to the notifier API.
  379. if False, the event will only be sent via
  380. notify_event_callbacks to in process listeners.
  381. """
  382. payload = {'resource_info': resource_id}
  383. if actor_dict:
  384. payload['actor_id'] = actor_dict['id']
  385. payload['actor_type'] = actor_dict['type']
  386. payload['actor_operation'] = actor_dict['actor_operation']
  387. if initiator:
  388. payload['request_id'] = initiator.request_id
  389. global_request_id = getattr(initiator, 'global_request_id', None)
  390. if global_request_id:
  391. payload['global_request_id'] = global_request_id
  392. notify_event_callbacks(SERVICE, resource_type, operation, payload)
  393. # Only send this notification if the 'basic' format is used, otherwise
  394. # let the CADF functions handle sending the notification. But we check
  395. # here so as to not disrupt the notify_event_callbacks function.
  396. if public and CONF.notification_format == 'basic':
  397. notifier = _get_notifier()
  398. if notifier:
  399. context = {}
  400. event_type = '%(service)s.%(resource_type)s.%(operation)s' % {
  401. 'service': SERVICE,
  402. 'resource_type': resource_type,
  403. 'operation': operation}
  404. if _check_notification_opt_out(event_type, outcome=None):
  405. return
  406. try:
  407. notifier.info(context, event_type, payload)
  408. except Exception:
  409. LOG.exception(
  410. 'Failed to send %(res_id)s %(event_type)s notification',
  411. {'res_id': resource_id, 'event_type': event_type})
  412. def _get_request_audit_info(context, user_id=None):
  413. """Collect audit information about the request used for CADF.
  414. :param context: Request context
  415. :param user_id: Optional user ID, alternatively collected from context
  416. :returns: Auditing data about the request
  417. :rtype: :class:`pycadf.Resource`
  418. """
  419. remote_addr = None
  420. http_user_agent = None
  421. project_id = None
  422. domain_id = None
  423. if context and 'environment' in context and context['environment']:
  424. environment = context['environment']
  425. remote_addr = environment.get('REMOTE_ADDR')
  426. http_user_agent = environment.get('HTTP_USER_AGENT')
  427. if not user_id:
  428. user_id = environment.get('KEYSTONE_AUTH_CONTEXT',
  429. {}).get('user_id')
  430. project_id = environment.get('KEYSTONE_AUTH_CONTEXT',
  431. {}).get('project_id')
  432. domain_id = environment.get('KEYSTONE_AUTH_CONTEXT',
  433. {}).get('domain_id')
  434. host = pycadf.host.Host(address=remote_addr, agent=http_user_agent)
  435. initiator = resource.Resource(typeURI=taxonomy.ACCOUNT_USER, host=host)
  436. if user_id:
  437. initiator.user_id = user_id
  438. initiator.id = utils.resource_uuid(user_id)
  439. initiator = _add_username_to_initiator(initiator)
  440. if project_id:
  441. initiator.project_id = project_id
  442. if domain_id:
  443. initiator.domain_id = domain_id
  444. return initiator
  445. class CadfNotificationWrapper(object):
  446. """Send CADF event notifications for various methods.
  447. This function is only used for Authentication events. Its ``action`` and
  448. ``event_type`` are dictated below.
  449. - action: ``authenticate``
  450. - event_type: ``identity.authenticate``
  451. Sends CADF notifications for events such as whether an authentication was
  452. successful or not.
  453. :param operation: The authentication related action being performed
  454. """
  455. def __init__(self, operation):
  456. self.action = operation
  457. self.event_type = '%s.%s' % (SERVICE, operation)
  458. def __call__(self, f):
  459. @functools.wraps(f)
  460. def wrapper(wrapped_self, user_id, *args, **kwargs):
  461. """Will always send a notification."""
  462. target = resource.Resource(typeURI=taxonomy.ACCOUNT_USER)
  463. initiator = build_audit_initiator()
  464. initiator.user_id = user_id
  465. initiator = _add_username_to_initiator(initiator)
  466. initiator.id = utils.resource_uuid(user_id)
  467. try:
  468. result = f(wrapped_self, user_id, *args, **kwargs)
  469. except (exception.AccountLocked,
  470. exception.PasswordExpired) as ex:
  471. # Send a CADF event with a reason for PCI-DSS related
  472. # authentication failures
  473. audit_reason = reason.Reason(str(ex), str(ex.code))
  474. _send_audit_notification(self.action, initiator,
  475. taxonomy.OUTCOME_FAILURE,
  476. target, self.event_type,
  477. reason=audit_reason)
  478. raise
  479. except Exception:
  480. # For authentication failure send a CADF event as well
  481. _send_audit_notification(self.action, initiator,
  482. taxonomy.OUTCOME_FAILURE,
  483. target, self.event_type)
  484. raise
  485. else:
  486. _send_audit_notification(self.action, initiator,
  487. taxonomy.OUTCOME_SUCCESS,
  488. target, self.event_type)
  489. return result
  490. return wrapper
  491. class CadfRoleAssignmentNotificationWrapper(object):
  492. """Send CADF notifications for ``role_assignment`` methods.
  493. This function is only used for role assignment events. Its ``action`` and
  494. ``event_type`` are dictated below.
  495. - action: ``created.role_assignment`` or ``deleted.role_assignment``
  496. - event_type: ``identity.role_assignment.created`` or
  497. ``identity.role_assignment.deleted``
  498. Sends a CADF notification if the wrapped method does not raise an
  499. :class:`Exception` (such as :class:`keystone.exception.NotFound`).
  500. :param operation: one of the values from ACTIONS (created or deleted)
  501. """
  502. ROLE_ASSIGNMENT = 'role_assignment'
  503. def __init__(self, operation):
  504. self.action = '%s.%s' % (operation, self.ROLE_ASSIGNMENT)
  505. self.event_type = '%s.%s.%s' % (SERVICE, self.ROLE_ASSIGNMENT,
  506. operation)
  507. def __call__(self, f):
  508. @functools.wraps(f)
  509. def wrapper(wrapped_self, role_id, *args, **kwargs):
  510. """Send a notification if the wrapped callable is successful.
  511. NOTE(stevemar): The reason we go through checking kwargs
  512. and args for possible target and actor values is because the
  513. create_grant() (and delete_grant()) method are called
  514. differently in various tests.
  515. Using named arguments, i.e.::
  516. create_grant(user_id=user['id'], domain_id=domain['id'],
  517. role_id=role['id'])
  518. Or, using positional arguments, i.e.::
  519. create_grant(role_id['id'], user['id'], None,
  520. domain_id=domain['id'], None)
  521. Or, both, i.e.::
  522. create_grant(role_id['id'], user_id=user['id'],
  523. domain_id=domain['id'])
  524. Checking the values for kwargs is easy enough, since it comes
  525. in as a dictionary
  526. The actual method signature is
  527. ::
  528. create_grant(role_id, user_id=None, group_id=None,
  529. domain_id=None, project_id=None,
  530. inherited_to_projects=False)
  531. So, if the values of actor or target are still None after
  532. checking kwargs, we can check the positional arguments,
  533. based on the method signature.
  534. """
  535. call_args = inspect.getcallargs(
  536. f, wrapped_self, role_id, *args, **kwargs)
  537. inherited = call_args['inherited_to_projects']
  538. initiator = call_args.get('initiator', None)
  539. target = resource.Resource(typeURI=taxonomy.ACCOUNT_USER)
  540. audit_kwargs = {}
  541. if call_args['project_id']:
  542. audit_kwargs['project'] = call_args['project_id']
  543. elif call_args['domain_id']:
  544. audit_kwargs['domain'] = call_args['domain_id']
  545. if call_args['user_id']:
  546. audit_kwargs['user'] = call_args['user_id']
  547. elif call_args['group_id']:
  548. audit_kwargs['group'] = call_args['group_id']
  549. audit_kwargs['inherited_to_projects'] = inherited
  550. audit_kwargs['role'] = role_id
  551. try:
  552. result = f(wrapped_self, role_id, *args, **kwargs)
  553. except Exception:
  554. _send_audit_notification(self.action, initiator,
  555. taxonomy.OUTCOME_FAILURE,
  556. target, self.event_type,
  557. **audit_kwargs)
  558. raise
  559. else:
  560. _send_audit_notification(self.action, initiator,
  561. taxonomy.OUTCOME_SUCCESS,
  562. target, self.event_type,
  563. **audit_kwargs)
  564. return result
  565. return wrapper
  566. def send_saml_audit_notification(action, user_id, group_ids,
  567. identity_provider, protocol, token_id,
  568. outcome):
  569. """Send notification to inform observers about SAML events.
  570. :param action: Action being audited
  571. :type action: str
  572. :param user_id: User ID from Keystone token
  573. :type user_id: str
  574. :param group_ids: List of Group IDs from Keystone token
  575. :type group_ids: list
  576. :param identity_provider: ID of the IdP from the Keystone token
  577. :type identity_provider: str or None
  578. :param protocol: Protocol ID for IdP from the Keystone token
  579. :type protocol: str
  580. :param token_id: audit_id from Keystone token
  581. :type token_id: str or None
  582. :param outcome: One of :class:`pycadf.cadftaxonomy`
  583. :type outcome: str
  584. """
  585. initiator = build_audit_initiator()
  586. target = resource.Resource(typeURI=taxonomy.ACCOUNT_USER)
  587. audit_type = SAML_AUDIT_TYPE
  588. user_id = user_id or taxonomy.UNKNOWN
  589. token_id = token_id or taxonomy.UNKNOWN
  590. group_ids = group_ids or []
  591. cred = credential.FederatedCredential(token=token_id, type=audit_type,
  592. identity_provider=identity_provider,
  593. user=user_id, groups=group_ids)
  594. initiator.credential = cred
  595. event_type = '%s.%s' % (SERVICE, action)
  596. _send_audit_notification(action, initiator, outcome, target, event_type)
  597. class _CatalogHelperObj(provider_api.ProviderAPIMixin, object):
  598. """A helper object to allow lookups of identity service id."""
  599. def _send_audit_notification(action, initiator, outcome, target,
  600. event_type, reason=None, **kwargs):
  601. """Send CADF notification to inform observers about the affected resource.
  602. This method logs an exception when sending the notification fails.
  603. :param action: CADF action being audited (e.g., 'authenticate')
  604. :param initiator: CADF resource representing the initiator
  605. :param outcome: The CADF outcome (taxonomy.OUTCOME_PENDING,
  606. taxonomy.OUTCOME_SUCCESS, taxonomy.OUTCOME_FAILURE)
  607. :param target: CADF resource representing the target
  608. :param event_type: An OpenStack-ism, typically this is the meter name that
  609. Ceilometer uses to poll events.
  610. :param kwargs: Any additional arguments passed in will be added as
  611. key-value pairs to the CADF event.
  612. :param reason: Reason for the notification which contains the response
  613. code and message description
  614. """
  615. if _check_notification_opt_out(event_type, outcome):
  616. return
  617. global _CATALOG_HELPER_OBJ
  618. if _CATALOG_HELPER_OBJ is None:
  619. _CATALOG_HELPER_OBJ = _CatalogHelperObj()
  620. service_list = _CATALOG_HELPER_OBJ.catalog_api.list_services()
  621. service_id = None
  622. for i in service_list:
  623. if i['type'] == SERVICE:
  624. service_id = i['id']
  625. break
  626. initiator = _add_username_to_initiator(initiator)
  627. event = eventfactory.EventFactory().new_event(
  628. eventType=cadftype.EVENTTYPE_ACTIVITY,
  629. outcome=outcome,
  630. action=action,
  631. initiator=initiator,
  632. target=target,
  633. reason=reason,
  634. observer=resource.Resource(typeURI=taxonomy.SERVICE_SECURITY))
  635. if service_id is not None:
  636. event.observer.id = service_id
  637. for key, value in kwargs.items():
  638. setattr(event, key, value)
  639. context = {}
  640. payload = event.as_dict()
  641. notifier = _get_notifier()
  642. if notifier:
  643. try:
  644. notifier.info(context, event_type, payload)
  645. except Exception:
  646. # diaper defense: any exception that occurs while emitting the
  647. # notification should not interfere with the API request
  648. LOG.exception(
  649. 'Failed to send %(action)s %(event_type)s notification',
  650. {'action': action, 'event_type': event_type})
  651. def _check_notification_opt_out(event_type, outcome):
  652. """Check if a particular event_type has been opted-out of.
  653. This method checks to see if an event should be sent to the messaging
  654. service. Any event specified in the opt-out list will not be transmitted.
  655. :param event_type: This is the meter name that Ceilometer uses to poll
  656. events. For example: identity.user.created, or
  657. identity.authenticate.success, or identity.role_assignment.created
  658. :param outcome: The CADF outcome (taxonomy.OUTCOME_PENDING,
  659. taxonomy.OUTCOME_SUCCESS, taxonomy.OUTCOME_FAILURE)
  660. """
  661. # NOTE(stevemar): Special handling for authenticate, we look at the outcome
  662. # as well when evaluating. For authN events, event_type is just
  663. # identity.authenticate, which isn't fine enough to provide any opt-out
  664. # value, so we attach the outcome to re-create the meter name used in
  665. # ceilometer.
  666. if 'authenticate' in event_type:
  667. event_type = event_type + "." + outcome
  668. if event_type in CONF.notification_opt_out:
  669. return True
  670. return False
  671. def _add_username_to_initiator(initiator):
  672. """Add the username to the initiator if missing."""
  673. if hasattr(initiator, 'username'):
  674. return initiator
  675. try:
  676. user_ref = PROVIDERS.identity_api.get_user(initiator.user_id)
  677. initiator.username = user_ref['name']
  678. except (exception.UserNotFound, AttributeError):
  679. # Either user not found or no user_id, move along
  680. pass
  681. return initiator
  682. emit_event = CadfNotificationWrapper
  683. role_assignment = CadfRoleAssignmentNotificationWrapper