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:
parent
29ffdcff49
commit
b2b341f470
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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'])
|
||||
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user