Adds Cloud Audit (CADF) Support for keystone authentication

This restores context passing to the manager layer which was originally
included to support auditing / logging (which we haven't supported until
now), and was later removed to support caching. However, we don't have
any reason to cache the results of authenticate() and it needs to be
audited. -dolphm

Change-Id: I2d43617f66fa2b23221dcfa4f7e935f64e458e1c
Implements: bp audit-event-record
DocImpact
This commit is contained in:
Brad Topol 2014-01-28 09:22:50 -06:00 committed by Dolph Mathews
parent 29ffdcff49
commit b2b341f470
10 changed files with 178 additions and 6 deletions

View File

@ -113,6 +113,7 @@ class Password(auth.AuthMethodHandler):
# all we care is password matches
try:
self.identity_api.authenticate(
context,
user_id=user_info.user_id,
password=user_info.password,
domain_scope=user_info.domain_id)

View File

@ -60,6 +60,7 @@ class UserController(identity.controllers.User):
try:
user_ref = self.identity_api.authenticate(
context,
user_id=user_id_from_token,
password=original_password)
if not user_ref.get('enabled', True):

View File

@ -350,8 +350,8 @@ class UserV3(controller.V3Controller):
domain_scope = self._get_domain_id_for_request(context)
try:
self.identity_api.change_password(user_id, original_password,
password, domain_scope)
self.identity_api.change_password(
context, user_id, original_password, password, domain_scope)
except AssertionError:
raise exception.Unauthorized()

View File

@ -270,8 +270,9 @@ class Manager(manager.Manager):
# - select the right driver for this domain
# - clear/set domain_ids for drivers that do not support domains
@notifications.emit_event('authenticate')
@domains_configured
def authenticate(self, user_id, password, domain_scope=None):
def authenticate(self, context, user_id, password, domain_scope=None):
domain_id, driver = self._get_domain_id_and_driver(domain_scope)
ref = driver.authenticate(user_id, password)
if not driver.is_domain_aware():
@ -462,11 +463,11 @@ class Manager(manager.Manager):
return driver.check_user_in_group(user_id, group_id)
@domains_configured
def change_password(self, user_id, original_password, new_password,
domain_scope):
def change_password(self, context, user_id, original_password,
new_password, domain_scope):
# authenticate() will raise an AssertionError if authentication fails
self.authenticate(user_id, original_password,
self.authenticate(context, user_id, original_password,
domain_scope=domain_scope)
update_dict = {'password': new_password}

View File

@ -19,6 +19,11 @@ import socket
from oslo.config import cfg
from oslo import messaging
import pycadf
from pycadf import cadftaxonomy as taxonomy
from pycadf import cadftype
from pycadf import eventfactory
from pycadf import resource
from keystone.openstack.common import log
@ -213,3 +218,90 @@ def _send_notification(operation, resource_type, resource_id, public=True):
LOG.exception(_(
'Failed to send %(res_id)s %(event_type)s notification'),
{'res_id': resource_id, 'event_type': event_type})
class CadfNotificationWrapper(object):
"""Send CADF event notifications for various methods.
Sends CADF notifications for events such as whether an authentication was
successful or not.
"""
def __init__(self, action):
self.action = action
def __call__(self, f):
def wrapper(wrapped_self, context, user_id, *args, **kwargs):
"""Always send a notification."""
remote_addr = None
http_user_agent = None
environment = context.get('environment')
if environment:
remote_addr = environment.get('REMOTE_ADDR')
http_user_agent = environment.get('HTTP_USER_AGENT')
host = pycadf.host.Host(address=remote_addr, agent=http_user_agent)
initiator = resource.Resource(typeURI=taxonomy.ACCOUNT_USER,
name=user_id, host=host)
_send_audit_notification(self.action, initiator,
taxonomy.OUTCOME_PENDING)
try:
result = f(wrapped_self, context, user_id, *args, **kwargs)
except Exception:
# For authentication failure send a cadf event as well
_send_audit_notification(self.action, initiator,
taxonomy.OUTCOME_FAILURE)
raise
else:
_send_audit_notification(self.action, initiator,
taxonomy.OUTCOME_SUCCESS)
return result
return wrapper
def _send_audit_notification(action, initiator, outcome):
"""Send CADF notification to inform observers about the affected resource.
This method logs an exception when sending the notification fails.
:param action: CADF action being audited (e.g., 'authenticate')
:param initiator: CADF resource representing the initiator
:param outcome: The CADF outcome (taxonomy.OUTCOME_PENDING,
taxonomy.OUTCOME_SUCCESS, taxonomy.OUTCOME_FAILURE)
"""
event = eventfactory.EventFactory().new_event(
eventType=cadftype.EVENTTYPE_ACTIVITY,
outcome=outcome,
action=action,
initiator=initiator,
target=resource.Resource(typeURI=taxonomy.ACCOUNT_USER),
observer=resource.Resource(typeURI='service/security'))
context = {}
payload = event.as_dict()
LOG.debug(_('CADF Event: %s'), payload)
service = 'identity'
event_type = '%(service)s.%(action)s' % {'service': service,
'action': action}
notifier = _get_notifier()
if notifier:
try:
notifier.info(context, event_type, payload)
except Exception:
# diaper defense: any exception that occurs while emitting the
# notification should not interfere with the API request
LOG.exception(_(
'Failed to send %(action)s %(event_type)s notification'),
{'action': action, 'event_type': event_type})
emit_event = CadfNotificationWrapper

View File

@ -79,17 +79,20 @@ class IdentityTests(object):
def test_authenticate_bad_user(self):
self.assertRaises(AssertionError,
self.identity_api.authenticate,
context={},
user_id=uuid.uuid4().hex,
password=self.user_foo['password'])
def test_authenticate_bad_password(self):
self.assertRaises(AssertionError,
self.identity_api.authenticate,
context={},
user_id=self.user_foo['id'],
password=uuid.uuid4().hex)
def test_authenticate(self):
user_ref = self.identity_api.authenticate(
context={},
user_id=self.user_sna['id'],
password=self.user_sna['password'])
# NOTE(termie): the password field is left in user_sna to make
@ -110,6 +113,7 @@ class IdentityTests(object):
self.assignment_api.add_user_to_project(self.tenant_baz['id'],
user['id'])
user_ref = self.identity_api.authenticate(
context={},
user_id=user['id'],
password=user['password'])
self.assertNotIn('password', user_ref)
@ -134,6 +138,7 @@ class IdentityTests(object):
self.assertRaises(AssertionError,
self.identity_api.authenticate,
context={},
user_id=id_,
password='password')
@ -1819,10 +1824,12 @@ class IdentityTests(object):
# with a password that is empty string or None
self.assertRaises(AssertionError,
self.identity_api.authenticate,
context={},
user_id='fake1',
password='')
self.assertRaises(AssertionError,
self.identity_api.authenticate,
context={},
user_id='fake1',
password=None)
@ -1835,10 +1842,12 @@ class IdentityTests(object):
# with a password that is empty string or None
self.assertRaises(AssertionError,
self.identity_api.authenticate,
context={},
user_id='fake1',
password='')
self.assertRaises(AssertionError,
self.identity_api.authenticate,
context={},
user_id='fake1',
password=None)

View File

@ -447,6 +447,7 @@ class BaseLDAPIdentity(test_backend.IdentityTests):
self.assertRaises(AssertionError,
self.identity_api.authenticate,
context={},
user_id=user['id'],
password=None,
domain_scope=user['domain_id'])

View File

@ -440,3 +440,68 @@ class TestEventCallbacks(test_v3.RestfulTestCase):
notifications.SUBSCRIBERS = {}
self.assertRaises(ValueError, Foo)
class CadfNotificationsWrapperTestCase(test_v3.RestfulTestCase):
LOCAL_HOST = 'localhost'
ACTION = 'authenticate'
def setUp(self):
super(CadfNotificationsWrapperTestCase, self).setUp()
self._notifications = []
def fake_notify(action, initiator, outcome):
note = {
'action': action,
'initiator': initiator,
# NOTE(stevemar): outcome has 2 stages, pending and success
# so we are ignoring it for now.
#'outcome': outcome,
'send_notification_called': True}
self._notifications.append(note)
# TODO(stevemar): Look into using mock instead of mox
fixture = self.useFixture(moxstubout.MoxStubout())
self.stubs = fixture.stubs
self.stubs.Set(notifications, '_send_audit_notification',
fake_notify)
def _assertLastNotify(self, action, user_id):
self.assertTrue(self._notifications)
note = self._notifications[-1]
self.assertEqual(note['action'], action)
initiator = note['initiator']
self.assertEqual(initiator.name, user_id)
self.assertEqual(initiator.host.address, self.LOCAL_HOST)
self.assertTrue(note['send_notification_called'])
def test_v3_authenticate_user_name_and_domain_id(self):
user_id = self.user_id
user_name = self.user['name']
password = self.user['password']
domain_id = self.domain_id
data = self.build_authentication_request(username=user_name,
user_domain_id=domain_id,
password=password)
self.post('/auth/tokens', body=data)
self._assertLastNotify(self.ACTION, user_id)
def test_v3_authenticate_user_id(self):
user_id = self.user_id
password = self.user['password']
data = self.build_authentication_request(user_id=user_id,
password=password)
self.post('/auth/tokens', body=data)
self._assertLastNotify(self.ACTION, user_id)
def test_v3_authenticate_user_name_and_domain_name(self):
user_id = self.user_id
user_name = self.user['name']
password = self.user['password']
domain_name = self.domain['name']
data = self.build_authentication_request(username=user_name,
user_domain_name=domain_name,
password=password)
self.post('/auth/tokens', body=data)
self._assertLastNotify(self.ACTION, user_id)

View File

@ -263,6 +263,7 @@ class Auth(controller.V2Controller):
try:
user_ref = self.identity_api.authenticate(
context,
user_id=user_id,
password=password)
except AssertionError as e:

View File

@ -21,6 +21,7 @@ Babel>=1.3
oauthlib>=0.6
dogpile.cache>=0.5.0
jsonschema>=2.0.0,<3.0.0
pycadf>=0.1.9
# KDS exclusive dependencies