Add audit support to keystone federation

implements bp audit-support-for-federation

Change-Id: Ifb1d8b593e8f387afd404367bd6c458243aa2695
This commit is contained in:
Brad Topol 2014-08-14 15:22:20 -05:00
parent e0d83776bd
commit a64bb88929
4 changed files with 129 additions and 19 deletions

View File

@ -10,6 +10,9 @@
# License for the specific language governing permissions and limitations
# under the License.
import functools
from pycadf import cadftaxonomy as taxonomy
from six.moves.urllib import parse
from keystone import auth
@ -17,6 +20,7 @@ from keystone.common import dependency
from keystone.contrib import federation
from keystone.contrib.federation import utils
from keystone.models import token_model
from keystone import notifications
from keystone.openstack.common import jsonutils
@ -38,27 +42,47 @@ class Mapped(auth.AuthMethodHandler):
"""
if 'id' in auth_payload:
fields = self._handle_scoped_token(auth_payload)
fields = self._handle_scoped_token(context, auth_payload)
else:
fields = self._handle_unscoped_token(context, auth_payload)
auth_context.update(fields)
def _handle_scoped_token(self, auth_payload):
def _handle_scoped_token(self, context, auth_payload):
token_id = auth_payload['id']
token_ref = token_model.KeystoneToken(
token_id=auth_payload['id'],
token_id=token_id,
token_data=self.token_provider_api.validate_token(
auth_payload['id']))
token_id))
utils.validate_expiration(token_ref)
mapping = self.federation_api.get_mapping_from_idp_and_protocol(
token_ref.federation_idp_id, token_ref.federation_protocol_id)
utils.validate_groups(token_ref.federation_group_ids,
mapping['id'], self.identity_api)
token_audit_id = token_ref.audit_id
identity_provider = token_ref.federation_idp_id
protocol = token_ref.federation_protocol_id
user_id = token_ref['user']['id']
group_ids = token_ref.federation_group_ids
send_notification = functools.partial(
notifications.send_saml_audit_notification, 'authenticate',
context, user_id, group_ids, identity_provider, protocol,
token_audit_id)
try:
mapping = self.federation_api.get_mapping_from_idp_and_protocol(
identity_provider, protocol)
utils.validate_groups(group_ids, mapping['id'], self.identity_api)
except Exception:
# NOTE(topol): Diaper defense to catch any exception, so we can
# send off failed authentication notification, raise the exception
# after sending the notification
send_notification(taxonomy.OUTCOME_FAILURE)
raise
else:
send_notification(taxonomy.OUTCOME_SUCCESS)
return {
'user_id': token_ref.user_id,
'group_ids': token_ref.federation_group_ids,
federation.IDENTITY_PROVIDER: token_ref.federation_idp_id,
federation.PROTOCOL: token_ref.federation_protocol_id
'user_id': user_id,
'group_ids': group_ids,
federation.IDENTITY_PROVIDER: identity_provider,
federation.PROTOCOL: protocol
}
def _handle_unscoped_token(self, context, auth_payload):
@ -67,17 +91,42 @@ class Mapped(auth.AuthMethodHandler):
assertion['user_id'] = user_id
identity_provider = auth_payload['identity_provider']
protocol = auth_payload['protocol']
group_ids = None
# NOTE(topol): Since the user is coming in from an IdP with a SAML doc
# instead of from a token we set token_id to None
token_id = None
mapped_properties = self._apply_mapping_filter(identity_provider,
protocol,
assertion)
try:
mapped_properties = self._apply_mapping_filter(identity_provider,
protocol,
assertion)
if not user_id:
user_id = parse.quote(mapped_properties['name'])
group_ids = mapped_properties['group_ids']
if not user_id:
user_id = parse.quote(mapped_properties['name'])
except Exception:
# NOTE(topol): Diaper defense to catch any exception, so we can
# send off failed authentication notification, raise the exception
# after sending the notification
outcome = taxonomy.OUTCOME_FAILURE
notifications.send_saml_audit_notification('authenticate', context,
user_id, group_ids,
identity_provider,
protocol, token_id,
outcome)
raise
else:
outcome = taxonomy.OUTCOME_SUCCESS
notifications.send_saml_audit_notification('authenticate', context,
user_id, group_ids,
identity_provider,
protocol, token_id,
outcome)
return {
'user_id': user_id,
'group_ids': mapped_properties['group_ids'],
'group_ids': group_ids,
federation.IDENTITY_PROVIDER: identity_provider,
federation.PROTOCOL: protocol
}

View File

@ -24,6 +24,7 @@ from oslo import messaging
import pycadf
from pycadf import cadftaxonomy as taxonomy
from pycadf import cadftype
from pycadf import credential
from pycadf import eventfactory
from pycadf import resource
@ -45,6 +46,7 @@ _ACTIONS = collections.namedtuple(
ACTIONS = _ACTIONS(created='created', deleted='deleted', disabled='disabled',
updated='updated', internal='internal')
SAML_AUDIT_TYPE = 'http://docs.oasis-open.org/security/saml/v2.0'
# resource types that can be notified
_SUBSCRIBERS = {}
_notifier = None
@ -402,6 +404,21 @@ class CadfRoleAssignmentNotificationWrapper(object):
return wrapper
def send_saml_audit_notification(action, context, user_id, group_ids,
identity_provider, protocol, token_id,
outcome):
initiator = _get_request_audit_info(context)
audit_type = SAML_AUDIT_TYPE
user_id = user_id or taxonomy.UNKNOWN
token_id = token_id or taxonomy.UNKNOWN
group_ids = group_ids or []
cred = credential.FederatedCredential(token=token_id, type=audit_type,
identity_provider=identity_provider,
user=user_id, groups=group_ids)
initiator.credential = cred
_send_audit_notification(action, initiator, outcome)
def _send_audit_notification(action, initiator, outcome, **kwargs):
"""Send CADF notification to inform observers about the affected resource.

View File

@ -13,6 +13,8 @@
import random
import uuid
from oslotest import mockpatch
from keystone.auth import controllers as auth_controllers
from keystone.common import dependency
from keystone.common import serializer
@ -20,6 +22,7 @@ from keystone import config
from keystone.contrib.federation import controllers as federation_controllers
from keystone.contrib.federation import utils as mapping_utils
from keystone import exception
from keystone import notifications
from keystone.openstack.common import jsonutils
from keystone.openstack.common import log
from keystone.tests import mapping_fixtures
@ -754,6 +757,26 @@ class MappingRuleEngineTests(FederationTests):
class FederatedTokenTests(FederationTests):
def setUp(self):
super(FederatedTokenTests, self).setUp()
self._notifications = []
def fake_saml_notify(action, context, user_id, group_ids,
identity_provider, protocol, token_id, outcome):
note = {
'action': action,
'user_id': user_id,
'identity_provider': identity_provider,
'protocol': protocol,
'send_notification_called': True}
self._notifications.append(note)
self.useFixture(mockpatch.PatchObject(
notifications,
'send_saml_audit_notification',
fake_saml_notify))
ACTION = 'authenticate'
IDP = 'ORG_IDP'
PROTOCOL = 'saml2'
AUTH_METHOD = 'saml2'
@ -770,6 +793,17 @@ class FederatedTokenTests(FederationTests):
}
}
def _assert_last_notify(self, action, identity_provider, protocol,
user_id=None):
self.assertTrue(self._notifications)
note = self._notifications[-1]
if user_id:
self.assertEqual(note['user_id'], user_id)
self.assertEqual(note['action'], action)
self.assertEqual(note['identity_provider'], identity_provider)
self.assertEqual(note['protocol'], protocol)
self.assertTrue(note['send_notification_called'])
def load_fixtures(self, fixtures):
super(FederationTests, self).load_fixtures(fixtures)
self.load_federation_sample_data()
@ -873,6 +907,10 @@ class FederatedTokenTests(FederationTests):
r = api.federated_authentication(context, self.IDP, self.PROTOCOL)
return r
def test_issue_unscoped_token_notify(self):
self._issue_unscoped_token()
self._assert_last_notify(self.ACTION, self.IDP, self.PROTOCOL)
def test_issue_unscoped_token(self):
r = self._issue_unscoped_token()
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
@ -926,6 +964,12 @@ class FederatedTokenTests(FederationTests):
r = api.authenticate_for_token(context, self.UNSCOPED_V3_SAML2_REQ)
self.assertIsNotNone(r.headers.get('X-Subject-Token'))
def test_scope_to_project_once_notify(self):
r = self.v3_authenticate_token(
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE)
user_id = r.json['token']['user']['id']
self._assert_last_notify(self.ACTION, self.IDP, self.PROTOCOL, user_id)
def test_scope_to_project_once(self):
r = self.v3_authenticate_token(
self.TOKEN_SCOPE_PROJECT_EMPLOYEE_FROM_EMPLOYEE)

View File

@ -23,5 +23,5 @@ Babel>=1.3
oauthlib>=0.6
dogpile.cache>=0.5.3
jsonschema>=2.0.0,<3.0.0
pycadf>=0.5.1
pycadf>=0.6.0
posix_ipc