From a64bb889292f07bd155fdf264933056b6e010ad0 Mon Sep 17 00:00:00 2001 From: Brad Topol Date: Thu, 14 Aug 2014 15:22:20 -0500 Subject: [PATCH] Add audit support to keystone federation implements bp audit-support-for-federation Change-Id: Ifb1d8b593e8f387afd404367bd6c458243aa2695 --- keystone/auth/plugins/mapped.py | 85 ++++++++++++++++++++++------ keystone/notifications.py | 17 ++++++ keystone/tests/test_v3_federation.py | 44 ++++++++++++++ requirements.txt | 2 +- 4 files changed, 129 insertions(+), 19 deletions(-) diff --git a/keystone/auth/plugins/mapped.py b/keystone/auth/plugins/mapped.py index a1ae5263a1..80aa664df3 100644 --- a/keystone/auth/plugins/mapped.py +++ b/keystone/auth/plugins/mapped.py @@ -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 } diff --git a/keystone/notifications.py b/keystone/notifications.py index c32f51812f..7182f20fd5 100644 --- a/keystone/notifications.py +++ b/keystone/notifications.py @@ -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. diff --git a/keystone/tests/test_v3_federation.py b/keystone/tests/test_v3_federation.py index 49c63db31e..426453c133 100644 --- a/keystone/tests/test_v3_federation.py +++ b/keystone/tests/test_v3_federation.py @@ -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) diff --git a/requirements.txt b/requirements.txt index e15a0c290e..4d3ea05d45 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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