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:
parent
d8938514fe
commit
8153a9d592
|
@ -26,6 +26,14 @@ For information about Identity API protection, see
|
||||||
<https://docs.openstack.org/keystone/latest/admin/service-api-protection.html>`_
|
<https://docs.openstack.org/keystone/latest/admin/service-api-protection.html>`_
|
||||||
in the OpenStack Cloud Administrator Guide.
|
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)
|
What's New in Version 3.13 (Train)
|
||||||
==================================
|
==================================
|
||||||
|
|
|
@ -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
|
assignments, or dynamically provisioning projects within keystone based on these
|
||||||
rules.
|
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.
|
An Identity Provider has exactly one mapping specified per protocol.
|
||||||
Mapping objects can be used multiple times by different combinations of Identity
|
Mapping objects can be used multiple times by different combinations of Identity
|
||||||
Provider and Protocol.
|
Provider and Protocol.
|
||||||
|
|
|
@ -28,9 +28,9 @@ _DISCOVERY_BLUEPRINT = flask.Blueprint('Discovery', __name__)
|
||||||
def _get_versions_list(identity_url):
|
def _get_versions_list(identity_url):
|
||||||
versions = {}
|
versions = {}
|
||||||
versions['v3'] = {
|
versions['v3'] = {
|
||||||
'id': 'v3.13',
|
'id': 'v3.14',
|
||||||
'status': 'stable',
|
'status': 'stable',
|
||||||
'updated': '2019-07-19T00:00:00Z',
|
'updated': '2020-04-07T00:00:00Z',
|
||||||
'links': [{
|
'links': [{
|
||||||
'rel': 'self',
|
'rel': 'self',
|
||||||
'href': identity_url,
|
'href': identity_url,
|
||||||
|
|
|
@ -235,10 +235,12 @@ def handle_unscoped_token(auth_payload, resource_api, federation_api,
|
||||||
get_user_unique_id_and_display_name(mapped_properties)
|
get_user_unique_id_and_display_name(mapped_properties)
|
||||||
)
|
)
|
||||||
email = mapped_properties['user'].get('email')
|
email = mapped_properties['user'].get('email')
|
||||||
user = identity_api.shadow_federated_user(identity_provider,
|
user = identity_api.shadow_federated_user(
|
||||||
protocol, unique_id,
|
identity_provider,
|
||||||
display_name,
|
protocol, unique_id,
|
||||||
email)
|
display_name,
|
||||||
|
email,
|
||||||
|
group_ids=mapped_properties['group_ids'])
|
||||||
|
|
||||||
if 'projects' in mapped_properties:
|
if 'projects' in mapped_properties:
|
||||||
idp_domain_id = federation_api.get_idp(
|
idp_domain_id = federation_api.get_idp(
|
||||||
|
|
|
@ -182,7 +182,7 @@ class Manager(manager.Manager):
|
||||||
self.driver.delete_protocol(idp_id, protocol_id)
|
self.driver.delete_protocol(idp_id, protocol_id)
|
||||||
|
|
||||||
for shadow_user in shadow_users:
|
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'],
|
PROVIDERS.identity_api, shadow_user['idp_id'],
|
||||||
shadow_user['protocol_id'], shadow_user['unique_id'],
|
shadow_user['protocol_id'], shadow_user['unique_id'],
|
||||||
shadow_user['display_name'],
|
shadow_user['display_name'],
|
||||||
|
|
|
@ -1135,7 +1135,7 @@ class Manager(manager.Manager):
|
||||||
self.get_user_by_name.invalidate(self, user_old['name'],
|
self.get_user_by_name.invalidate(self, user_old['name'],
|
||||||
user_old['domain_id'])
|
user_old['domain_id'])
|
||||||
for fed_user in fed_users:
|
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'],
|
self, fed_user['idp_id'], fed_user['protocol_id'],
|
||||||
fed_user['unique_id'], fed_user['display_name'],
|
fed_user['unique_id'], fed_user['display_name'],
|
||||||
user_old.get('extra', {}).get('email'))
|
user_old.get('extra', {}).get('email'))
|
||||||
|
@ -1402,18 +1402,8 @@ class Manager(manager.Manager):
|
||||||
return PROVIDERS.shadow_users_api.create_nonlocal_user(user)
|
return PROVIDERS.shadow_users_api.create_nonlocal_user(user)
|
||||||
|
|
||||||
@MEMOIZE
|
@MEMOIZE
|
||||||
def shadow_federated_user(self, idp_id, protocol_id, unique_id,
|
def _shadow_federated_user(self, idp_id, protocol_id, unique_id,
|
||||||
display_name, email=None):
|
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
|
|
||||||
"""
|
|
||||||
user_dict = {}
|
user_dict = {}
|
||||||
try:
|
try:
|
||||||
PROVIDERS.shadow_users_api.update_federated_user_display_name(
|
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'])
|
PROVIDERS.shadow_users_api.set_last_active_at(user_dict['id'])
|
||||||
return user_dict
|
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):
|
class MappingManager(manager.Manager):
|
||||||
"""Default pivot point for the ID Mapping backend."""
|
"""Default pivot point for the ID Mapping backend."""
|
||||||
|
|
|
@ -672,6 +672,41 @@ class SqlIdentity(SqlTests,
|
||||||
negative_user['id'])
|
negative_user['id'])
|
||||||
self.assertEqual(0, len(group_refs))
|
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):
|
def test_add_user_to_group_expiring(self):
|
||||||
self._build_fed_resource()
|
self._build_fed_resource()
|
||||||
domain = self._get_domain_fixture()
|
domain = self._get_domain_fixture()
|
||||||
|
|
|
@ -36,9 +36,9 @@ v3_MEDIA_TYPES = [
|
||||||
]
|
]
|
||||||
|
|
||||||
v3_EXPECTED_RESPONSE = {
|
v3_EXPECTED_RESPONSE = {
|
||||||
"id": "v3.13",
|
"id": "v3.14",
|
||||||
"status": "stable",
|
"status": "stable",
|
||||||
"updated": "2019-07-19T00:00:00Z",
|
"updated": "2020-04-07T00:00:00Z",
|
||||||
"links": [
|
"links": [
|
||||||
{
|
{
|
||||||
"rel": "self",
|
"rel": "self",
|
||||||
|
|
|
@ -12,4 +12,4 @@
|
||||||
|
|
||||||
|
|
||||||
def release_string():
|
def release_string():
|
||||||
return 'v3.13'
|
return 'v3.14'
|
||||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue