diff --git a/api-ref/source/v3/index.rst b/api-ref/source/v3/index.rst index 5b0e5ef89e..98757b3288 100644 --- a/api-ref/source/v3/index.rst +++ b/api-ref/source/v3/index.rst @@ -26,6 +26,14 @@ For information about Identity API protection, see `_ 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) ================================== diff --git a/doc/source/admin/federation/configure_federation.rst b/doc/source/admin/federation/configure_federation.rst index 92af44e010..e6f1740f5e 100644 --- a/doc/source/admin/federation/configure_federation.rst +++ b/doc/source/admin/federation/configure_federation.rst @@ -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. diff --git a/keystone/api/discovery.py b/keystone/api/discovery.py index 31f25d40c6..7483597b81 100644 --- a/keystone/api/discovery.py +++ b/keystone/api/discovery.py @@ -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, diff --git a/keystone/auth/plugins/mapped.py b/keystone/auth/plugins/mapped.py index 36f6366204..7a45f109be 100644 --- a/keystone/auth/plugins/mapped.py +++ b/keystone/auth/plugins/mapped.py @@ -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( diff --git a/keystone/federation/core.py b/keystone/federation/core.py index 559e2b58a8..5c268c5e18 100644 --- a/keystone/federation/core.py +++ b/keystone/federation/core.py @@ -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'], diff --git a/keystone/identity/core.py b/keystone/identity/core.py index 4aa489a12a..f9af5c4c21 100644 --- a/keystone/identity/core.py +++ b/keystone/identity/core.py @@ -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.""" diff --git a/keystone/tests/unit/test_backend_sql.py b/keystone/tests/unit/test_backend_sql.py index c0bff3aaa2..82285fcf8a 100644 --- a/keystone/tests/unit/test_backend_sql.py +++ b/keystone/tests/unit/test_backend_sql.py @@ -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() diff --git a/keystone/tests/unit/test_versions.py b/keystone/tests/unit/test_versions.py index 2fcea3f034..b509d2446e 100644 --- a/keystone/tests/unit/test_versions.py +++ b/keystone/tests/unit/test_versions.py @@ -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", diff --git a/keystone/version.py b/keystone/version.py index 7937efc0af..3cc1c97580 100644 --- a/keystone/version.py +++ b/keystone/version.py @@ -12,4 +12,4 @@ def release_string(): - return 'v3.13' + return 'v3.14' diff --git a/releasenotes/notes/bug-1809116-b65502f3b606b060.yaml b/releasenotes/notes/bug-1809116-b65502f3b606b060.yaml new file mode 100644 index 0000000000..d6067ba8ea --- /dev/null +++ b/releasenotes/notes/bug-1809116-b65502f3b606b060.yaml @@ -0,0 +1,19 @@ +--- +features: + - | + [`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.