Add expiring user group memberships on mapped authentication

When a federated user authenticates, they are added to their
mapped groups during shadowing.

Closes-Bug: 1809116

Change-Id: I19dc400b2a7aa46709b242cdeef82beaca975ff3
This commit is contained in:
Kristi Nikolla 2020-02-07 11:02:51 -05:00
parent d8938514fe
commit 8153a9d592
10 changed files with 111 additions and 23 deletions

View File

@ -26,6 +26,14 @@ For information about Identity API protection, see
<https://docs.openstack.org/keystone/latest/admin/service-api-protection.html>`_
in the OpenStack Cloud Administrator Guide.
===================================
What's New in Version 3.14 (Ussuri)
===================================
- New attribute ``authorization_ttl`` for identity providers
- New attribute ``membership_expires_at`` when listing groups for a user
- Ability to persist group memberships carried through mapping for a federated user
==================================
What's New in Version 3.13 (Train)
==================================

View File

@ -143,6 +143,17 @@ associating them with a local keystone group and inheriting its role
assignments, or dynamically provisioning projects within keystone based on these
rules.
.. note::
By default, group memberships that a user gets from a mapping are only valid
for the duration of the token. It is possible to persist these groups
memberships for a limited period of time. To enable this, either
set the ``authorization_ttl` attribute of the identity provider, or the
``[federation] default_authorization_ttl`` in the keystone.conf file. This
value is in minutes, and will result in a lag from when a user is removed
from a group in the identity provider, and when that will happen in keystone.
Please consider your security requirements carefully.
An Identity Provider has exactly one mapping specified per protocol.
Mapping objects can be used multiple times by different combinations of Identity
Provider and Protocol.

View File

@ -28,9 +28,9 @@ _DISCOVERY_BLUEPRINT = flask.Blueprint('Discovery', __name__)
def _get_versions_list(identity_url):
versions = {}
versions['v3'] = {
'id': 'v3.13',
'id': 'v3.14',
'status': 'stable',
'updated': '2019-07-19T00:00:00Z',
'updated': '2020-04-07T00:00:00Z',
'links': [{
'rel': 'self',
'href': identity_url,

View File

@ -235,10 +235,12 @@ def handle_unscoped_token(auth_payload, resource_api, federation_api,
get_user_unique_id_and_display_name(mapped_properties)
)
email = mapped_properties['user'].get('email')
user = identity_api.shadow_federated_user(identity_provider,
protocol, unique_id,
display_name,
email)
user = identity_api.shadow_federated_user(
identity_provider,
protocol, unique_id,
display_name,
email,
group_ids=mapped_properties['group_ids'])
if 'projects' in mapped_properties:
idp_domain_id = federation_api.get_idp(

View File

@ -182,7 +182,7 @@ class Manager(manager.Manager):
self.driver.delete_protocol(idp_id, protocol_id)
for shadow_user in shadow_users:
PROVIDERS.identity_api.shadow_federated_user.invalidate(
PROVIDERS.identity_api._shadow_federated_user.invalidate(
PROVIDERS.identity_api, shadow_user['idp_id'],
shadow_user['protocol_id'], shadow_user['unique_id'],
shadow_user['display_name'],

View File

@ -1135,7 +1135,7 @@ class Manager(manager.Manager):
self.get_user_by_name.invalidate(self, user_old['name'],
user_old['domain_id'])
for fed_user in fed_users:
self.shadow_federated_user.invalidate(
self._shadow_federated_user.invalidate(
self, fed_user['idp_id'], fed_user['protocol_id'],
fed_user['unique_id'], fed_user['display_name'],
user_old.get('extra', {}).get('email'))
@ -1402,18 +1402,8 @@ class Manager(manager.Manager):
return PROVIDERS.shadow_users_api.create_nonlocal_user(user)
@MEMOIZE
def shadow_federated_user(self, idp_id, protocol_id, unique_id,
display_name, email=None):
"""Map a federated user to a user.
:param idp_id: identity provider id
:param protocol_id: protocol id
:param unique_id: unique id for the user within the IdP
:param display_name: user's display name
:param email: user's email
:returns: dictionary of the mapped User entity
"""
def _shadow_federated_user(self, idp_id, protocol_id, unique_id,
display_name, email=None):
user_dict = {}
try:
PROVIDERS.shadow_users_api.update_federated_user_display_name(
@ -1440,6 +1430,29 @@ class Manager(manager.Manager):
PROVIDERS.shadow_users_api.set_last_active_at(user_dict['id'])
return user_dict
def shadow_federated_user(self, idp_id, protocol_id, unique_id,
display_name, email=None, group_ids=None):
"""Map a federated user to a user.
:param idp_id: identity provider id
:param protocol_id: protocol id
:param unique_id: unique id for the user within the IdP
:param display_name: user's display name
:param email: user's email
:param group_ids: list of group ids to add the user to
:returns: dictionary of the mapped User entity
"""
user_dict = self._shadow_federated_user(
idp_id, protocol_id, unique_id, display_name, email)
# Note(knikolla): The shadowing operation can be cached,
# however we need to update the expiring group memberships.
if group_ids:
for group_id in group_ids:
PROVIDERS.shadow_users_api.add_user_to_group_expires(
user_dict['id'], group_id)
return user_dict
class MappingManager(manager.Manager):
"""Default pivot point for the ID Mapping backend."""

View File

@ -672,6 +672,41 @@ class SqlIdentity(SqlTests,
negative_user['id'])
self.assertEqual(0, len(group_refs))
def test_add_user_to_group_expiring_mapped(self):
self._build_fed_resource()
domain = self._get_domain_fixture()
self.config_fixture.config(group='federation',
default_authorization_ttl=5)
time = datetime.datetime.utcnow()
tick = datetime.timedelta(minutes=5)
new_group = unit.new_group_ref(domain_id=domain['id'])
new_group = PROVIDERS.identity_api.create_group(new_group)
fed_dict = unit.new_federated_user_ref()
fed_dict['idp_id'] = 'myidp'
fed_dict['protocol_id'] = 'mapped'
with freezegun.freeze_time(time - tick) as frozen_time:
user = PROVIDERS.identity_api.shadow_federated_user(
**fed_dict, group_ids=[new_group['id']])
PROVIDERS.identity_api.check_user_in_group(user['id'],
new_group['id'])
# Expiration
frozen_time.tick(tick)
self.assertRaises(exception.NotFound,
PROVIDERS.identity_api.check_user_in_group,
user['id'],
new_group['id'])
# Renewal
PROVIDERS.identity_api.shadow_federated_user(
**fed_dict, group_ids=[new_group['id']])
PROVIDERS.identity_api.check_user_in_group(user['id'],
new_group['id'])
def test_add_user_to_group_expiring(self):
self._build_fed_resource()
domain = self._get_domain_fixture()

View File

@ -36,9 +36,9 @@ v3_MEDIA_TYPES = [
]
v3_EXPECTED_RESPONSE = {
"id": "v3.13",
"id": "v3.14",
"status": "stable",
"updated": "2019-07-19T00:00:00Z",
"updated": "2020-04-07T00:00:00Z",
"links": [
{
"rel": "self",

View File

@ -12,4 +12,4 @@
def release_string():
return 'v3.13'
return 'v3.14'

View File

@ -0,0 +1,19 @@
---
features:
- |
[`bug 1809116 <https://bugs.launchpad.net/keystone/+bug/1809116>`_]
It is now possible to have group memberships carried over through mapping
persist for a limited time after a user authenticates using federation.
The "time to live" of these memberships is specified via the configuration
option `[federation] default_authorization_ttl` or for each identity
provider by setting `authorization_ttl` on the identity provider. Every
time a user authenticates carrying over that membership, it will be
renewed.
security:
- |
If expiring user group memberships are enabled via the `[federation]
default_authorization_ttl` configuration option, or on an idp by idp
basis by setting `authorization_ttl`, there will be a lag between when
a user is removed from a group in an identity provider, and when that
will be reflected in keystone. That amount of time will be equal to
the last time the user logged in + idp ttl.