"Fossies" - the Fresh Open Source Software Archive

Member "keystone-16.0.2/keystone/notifications.py" (7 Jun 2021, 33037 Bytes) of package /linux/misc/openstack/keystone-16.0.2.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "notifications.py" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 16.0.1_vs_16.0.2.

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