Expiring Group Membership Driver - Add, List Groups
Modify the base driver and SQL driver to support expiring group memberships. Additions to the SQL Driver to support listing expiring groups for user. Change-Id: I7d52cd2003f511483619a429de57201df4990209 Partial-Bug: 1809116 Depends-On: I4294a879071dde07e5eb1da4df133de8032e1059
This commit is contained in:
parent
ee54ba0ce4
commit
d8938514fe
@ -1244,6 +1244,14 @@ links_user:
|
|||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: object
|
type: object
|
||||||
|
membership_expires_at_response_body:
|
||||||
|
description: |
|
||||||
|
The date and time when the group membership expires.
|
||||||
|
A ``null`` value indicates that the membership never expires.
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
min_version: 3.14
|
||||||
original_password:
|
original_password:
|
||||||
description: |
|
description: |
|
||||||
The original password for the user.
|
The original password for the user.
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
"links": {
|
"links": {
|
||||||
"self": "https://example.com/identity/v3/groups/ea167b"
|
"self": "https://example.com/identity/v3/groups/ea167b"
|
||||||
},
|
},
|
||||||
|
"membership_expires_at": null,
|
||||||
"name": "Developers"
|
"name": "Developers"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -16,6 +17,7 @@
|
|||||||
"links": {
|
"links": {
|
||||||
"self": "https://example.com/identity/v3/groups/a62db1"
|
"self": "https://example.com/identity/v3/groups/a62db1"
|
||||||
},
|
},
|
||||||
|
"membership_expires_at": "2016-11-06T15:32:17.000000",
|
||||||
"name": "Secure Developers"
|
"name": "Secure Developers"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -343,6 +343,20 @@ Parameters
|
|||||||
Response
|
Response
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- links: link_collection
|
||||||
|
- groups: groups
|
||||||
|
- description: group_description_response_body
|
||||||
|
- domain_id: group_domain_id_response_body
|
||||||
|
- id: group_id_response_body
|
||||||
|
- links: link_response_body
|
||||||
|
- name: group_name_response_body
|
||||||
|
- membership_expires_at: membership_expires_at_response_body
|
||||||
|
|
||||||
Status Codes
|
Status Codes
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -160,6 +160,13 @@ class Identity(base.IdentityDriverBase):
|
|||||||
'password_expires_at']
|
'password_expires_at']
|
||||||
return query, hints
|
return query, hints
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _apply_limits_to_list(collection, hints):
|
||||||
|
if not hints.limit:
|
||||||
|
return collection
|
||||||
|
|
||||||
|
return collection[:hints.limit['limit']]
|
||||||
|
|
||||||
@driver_hints.truncated
|
@driver_hints.truncated
|
||||||
def list_users(self, hints):
|
def list_users(self, hints):
|
||||||
with sql.session_for_read() as session:
|
with sql.session_for_read() as session:
|
||||||
@ -281,10 +288,24 @@ class Identity(base.IdentityDriverBase):
|
|||||||
with sql.session_for_read() as session:
|
with sql.session_for_read() as session:
|
||||||
self.get_group(group_id)
|
self.get_group(group_id)
|
||||||
self.get_user(user_id)
|
self.get_user(user_id)
|
||||||
|
|
||||||
|
# Note(knikolla): Check for normal group membership
|
||||||
query = session.query(model.UserGroupMembership)
|
query = session.query(model.UserGroupMembership)
|
||||||
query = query.filter_by(user_id=user_id)
|
query = query.filter_by(user_id=user_id)
|
||||||
query = query.filter_by(group_id=group_id)
|
query = query.filter_by(group_id=group_id)
|
||||||
if not query.first():
|
if query.first():
|
||||||
|
return
|
||||||
|
|
||||||
|
# Note(knikolla): Check for expiring group membership
|
||||||
|
query = session.query(model.ExpiringUserGroupMembership)
|
||||||
|
query = query.filter(
|
||||||
|
model.ExpiringUserGroupMembership.user_id == user_id)
|
||||||
|
query = query.filter(
|
||||||
|
model.ExpiringUserGroupMembership.group_id == group_id)
|
||||||
|
active = [q for q in query.all() if not q.expired]
|
||||||
|
if active:
|
||||||
|
return
|
||||||
|
|
||||||
raise exception.NotFound(_("User '%(user_id)s' not found in"
|
raise exception.NotFound(_("User '%(user_id)s' not found in"
|
||||||
" group '%(group_id)s'") %
|
" group '%(group_id)s'") %
|
||||||
{'user_id': user_id,
|
{'user_id': user_id,
|
||||||
@ -310,12 +331,33 @@ class Identity(base.IdentityDriverBase):
|
|||||||
session.delete(membership_ref)
|
session.delete(membership_ref)
|
||||||
|
|
||||||
def list_groups_for_user(self, user_id, hints):
|
def list_groups_for_user(self, user_id, hints):
|
||||||
|
def row_to_group_dict(row):
|
||||||
|
group = row.group.to_dict()
|
||||||
|
group['membership_expires_at'] = row.expires
|
||||||
|
return group
|
||||||
|
|
||||||
with sql.session_for_read() as session:
|
with sql.session_for_read() as session:
|
||||||
self.get_user(user_id)
|
self.get_user(user_id)
|
||||||
query = session.query(model.Group).join(model.UserGroupMembership)
|
query = session.query(model.Group).join(model.UserGroupMembership)
|
||||||
query = query.filter(model.UserGroupMembership.user_id == user_id)
|
query = query.filter(model.UserGroupMembership.user_id == user_id)
|
||||||
query = sql.filter_limit_query(model.Group, query, hints)
|
query = sql.filter_limit_query(model.Group, query, hints)
|
||||||
return [g.to_dict() for g in query]
|
groups = [g.to_dict() for g in query]
|
||||||
|
|
||||||
|
# Note(knikolla): We must use the ExpiringGroupMembership model
|
||||||
|
# so that we can access the expired property.
|
||||||
|
query = session.query(model.ExpiringUserGroupMembership)
|
||||||
|
query = query.filter(
|
||||||
|
model.ExpiringUserGroupMembership.user_id == user_id)
|
||||||
|
query = sql.filter_limit_query(
|
||||||
|
model.UserGroupMembership, query, hints)
|
||||||
|
expiring_groups = [row_to_group_dict(r) for r in query.all()
|
||||||
|
if not r.expired]
|
||||||
|
|
||||||
|
# Note(knikolla): I would have loved to be able to merge the two
|
||||||
|
# queries together and use filter_limit_query on the union, but
|
||||||
|
# I haven't found a generic way to express expiration in a SQL
|
||||||
|
# query, therefore we have to apply the limits here again.
|
||||||
|
return self._apply_limits_to_list(groups + expiring_groups, hints)
|
||||||
|
|
||||||
def list_users_in_group(self, group_id, hints):
|
def list_users_in_group(self, group_id, hints):
|
||||||
with sql.session_for_read() as session:
|
with sql.session_for_read() as session:
|
||||||
|
@ -1309,6 +1309,9 @@ class Manager(manager.Manager):
|
|||||||
# driver selection, so remove any such filter
|
# driver selection, so remove any such filter
|
||||||
self._mark_domain_id_filter_satisfied(hints)
|
self._mark_domain_id_filter_satisfied(hints)
|
||||||
ref_list = driver.list_groups_for_user(entity_id, hints)
|
ref_list = driver.list_groups_for_user(entity_id, hints)
|
||||||
|
for ref in ref_list:
|
||||||
|
if 'membership_expires_at' not in ref:
|
||||||
|
ref['membership_expires_at'] = None
|
||||||
return self._set_domain_id_and_mapping(
|
return self._set_domain_id_and_mapping(
|
||||||
ref_list, domain_id, driver, mapping.EntityType.GROUP)
|
ref_list, domain_id, driver, mapping.EntityType.GROUP)
|
||||||
|
|
||||||
|
@ -195,3 +195,32 @@ class ShadowUsers(base.ShadowUsersDriverBase):
|
|||||||
fed_user_refs = sql.filter_limit_query(model.FederatedUser, query,
|
fed_user_refs = sql.filter_limit_query(model.FederatedUser, query,
|
||||||
hints)
|
hints)
|
||||||
return [x.to_dict() for x in fed_user_refs]
|
return [x.to_dict() for x in fed_user_refs]
|
||||||
|
|
||||||
|
def add_user_to_group_expires(self, user_id, group_id):
|
||||||
|
def get_federated_user():
|
||||||
|
with sql.session_for_read() as session:
|
||||||
|
query = session.query(model.FederatedUser)
|
||||||
|
query = query.filter_by(user_id=user_id)
|
||||||
|
user = query.first()
|
||||||
|
if not user:
|
||||||
|
# Note(knikolla): This shouldn't really ever happen, since
|
||||||
|
# this requires the user to already be logged in.
|
||||||
|
raise exception.UserNotFound()
|
||||||
|
return user
|
||||||
|
|
||||||
|
with sql.session_for_write() as session:
|
||||||
|
user = get_federated_user()
|
||||||
|
query = session.query(model.ExpiringUserGroupMembership)
|
||||||
|
query = query.filter_by(user_id=user_id)
|
||||||
|
query = query.filter_by(group_id=group_id)
|
||||||
|
membership = query.first()
|
||||||
|
|
||||||
|
if membership:
|
||||||
|
membership.last_verified = datetime.datetime.utcnow()
|
||||||
|
else:
|
||||||
|
session.add(model.ExpiringUserGroupMembership(
|
||||||
|
user_id=user_id,
|
||||||
|
group_id=group_id,
|
||||||
|
idp_id=user.idp_id,
|
||||||
|
last_verified=datetime.datetime.utcnow()
|
||||||
|
))
|
||||||
|
@ -984,6 +984,8 @@ class BaseLDAPIdentity(LDAPTestSetup, IdentityTests, AssignmentTests,
|
|||||||
|
|
||||||
# List groups for user.
|
# List groups for user.
|
||||||
ref_list = PROVIDERS.identity_api.list_groups_for_user(public_user_id)
|
ref_list = PROVIDERS.identity_api.list_groups_for_user(public_user_id)
|
||||||
|
for ref in ref_list:
|
||||||
|
del(ref['membership_expires_at'])
|
||||||
|
|
||||||
group['id'] = public_group_id
|
group['id'] = public_group_id
|
||||||
self.assertThat(ref_list, matchers.Equals([group]))
|
self.assertThat(ref_list, matchers.Equals([group]))
|
||||||
|
@ -16,6 +16,7 @@ import datetime
|
|||||||
from unittest import mock
|
from unittest import mock
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
import freezegun
|
||||||
from oslo_db import exception as db_exception
|
from oslo_db import exception as db_exception
|
||||||
from oslo_db import options
|
from oslo_db import options
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
@ -671,6 +672,94 @@ 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(self):
|
||||||
|
self._build_fed_resource()
|
||||||
|
domain = self._get_domain_fixture()
|
||||||
|
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'
|
||||||
|
new_user = PROVIDERS.shadow_users_api.create_federated_user(
|
||||||
|
domain['id'], fed_dict
|
||||||
|
)
|
||||||
|
|
||||||
|
with freezegun.freeze_time(time - tick) as frozen_time:
|
||||||
|
PROVIDERS.shadow_users_api.add_user_to_group_expires(
|
||||||
|
new_user['id'], new_group['id'])
|
||||||
|
|
||||||
|
self.config_fixture.config(group='federation',
|
||||||
|
default_authorization_ttl=0)
|
||||||
|
self.assertRaises(exception.NotFound,
|
||||||
|
PROVIDERS.identity_api.check_user_in_group,
|
||||||
|
new_user['id'],
|
||||||
|
new_group['id'])
|
||||||
|
|
||||||
|
self.config_fixture.config(group='federation',
|
||||||
|
default_authorization_ttl=5)
|
||||||
|
PROVIDERS.identity_api.check_user_in_group(new_user['id'],
|
||||||
|
new_group['id'])
|
||||||
|
|
||||||
|
# Expiration
|
||||||
|
frozen_time.tick(tick)
|
||||||
|
self.assertRaises(exception.NotFound,
|
||||||
|
PROVIDERS.identity_api.check_user_in_group,
|
||||||
|
new_user['id'],
|
||||||
|
new_group['id'])
|
||||||
|
|
||||||
|
# Renewal
|
||||||
|
PROVIDERS.shadow_users_api.add_user_to_group_expires(
|
||||||
|
new_user['id'], new_group['id'])
|
||||||
|
PROVIDERS.identity_api.check_user_in_group(new_user['id'],
|
||||||
|
new_group['id'])
|
||||||
|
|
||||||
|
def test_add_user_to_group_expiring_list(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)
|
||||||
|
exp_new_group = unit.new_group_ref(domain_id=domain['id'])
|
||||||
|
exp_new_group = PROVIDERS.identity_api.create_group(exp_new_group)
|
||||||
|
|
||||||
|
fed_dict = unit.new_federated_user_ref()
|
||||||
|
fed_dict['idp_id'] = 'myidp'
|
||||||
|
fed_dict['protocol_id'] = 'mapped'
|
||||||
|
new_user = PROVIDERS.shadow_users_api.create_federated_user(
|
||||||
|
domain['id'], fed_dict
|
||||||
|
)
|
||||||
|
|
||||||
|
PROVIDERS.identity_api.add_user_to_group(new_user['id'],
|
||||||
|
new_group['id'])
|
||||||
|
PROVIDERS.identity_api.check_user_in_group(new_user['id'],
|
||||||
|
new_group['id'])
|
||||||
|
|
||||||
|
with freezegun.freeze_time(time - tick) as frozen_time:
|
||||||
|
PROVIDERS.shadow_users_api.add_user_to_group_expires(
|
||||||
|
new_user['id'], exp_new_group['id'])
|
||||||
|
PROVIDERS.identity_api.check_user_in_group(new_user['id'],
|
||||||
|
new_group['id'])
|
||||||
|
|
||||||
|
groups = PROVIDERS.identity_api.list_groups_for_user(
|
||||||
|
new_user['id'])
|
||||||
|
self.assertEqual(len(groups), 2)
|
||||||
|
for group in groups:
|
||||||
|
if group.get('membership_expires_at'):
|
||||||
|
self.assertEqual(group['membership_expires_at'], time)
|
||||||
|
|
||||||
|
frozen_time.tick(tick)
|
||||||
|
groups = PROVIDERS.identity_api.list_groups_for_user(
|
||||||
|
new_user['id'])
|
||||||
|
self.assertEqual(len(groups), 1)
|
||||||
|
|
||||||
def test_storing_null_domain_id_in_project_ref(self):
|
def test_storing_null_domain_id_in_project_ref(self):
|
||||||
"""Test the special storage of domain_id=None in sql resource driver.
|
"""Test the special storage of domain_id=None in sql resource driver.
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user