Add audit support to keystone federation
implements bp audit-support-for-federation Change-Id: Ifb1d8b593e8f387afd404367bd6c458243aa2695
This commit is contained in:
parent
e0d83776bd
commit
a64bb88929
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue