Browse Source

Merge "Expiring Group Membership Driver - Add, List Groups"

tags/17.0.0.0rc1
Zuul 2 months ago
committed by Gerrit Code Review
parent
commit
f5fd13f26f
8 changed files with 195 additions and 6 deletions
  1. +8
    -0
      api-ref/source/v3/parameters.yaml
  2. +2
    -0
      api-ref/source/v3/samples/admin/user-groups-list-response.json
  3. +14
    -0
      api-ref/source/v3/users.inc
  4. +48
    -6
      keystone/identity/backends/sql.py
  5. +3
    -0
      keystone/identity/core.py
  6. +29
    -0
      keystone/identity/shadow_backends/sql.py
  7. +2
    -0
      keystone/tests/unit/test_backend_ldap.py
  8. +89
    -0
      keystone/tests/unit/test_backend_sql.py

+ 8
- 0
api-ref/source/v3/parameters.yaml View File

@@ -1244,6 +1244,14 @@ links_user:
in: body
required: true
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:
description: |
The original password for the user.


+ 2
- 0
api-ref/source/v3/samples/admin/user-groups-list-response.json View File

@@ -7,6 +7,7 @@
"links": {
"self": "https://example.com/identity/v3/groups/ea167b"
},
"membership_expires_at": null,
"name": "Developers"
},
{
@@ -16,6 +17,7 @@
"links": {
"self": "https://example.com/identity/v3/groups/a62db1"
},
"membership_expires_at": "2016-11-06T15:32:17.000000",
"name": "Secure Developers"
}
],


+ 14
- 0
api-ref/source/v3/users.inc View File

@@ -343,6 +343,20 @@ Parameters
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
~~~~~~~~~~~~



+ 48
- 6
keystone/identity/backends/sql.py View File

@@ -160,6 +160,13 @@ class Identity(base.IdentityDriverBase):
'password_expires_at']
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
def list_users(self, hints):
with sql.session_for_read() as session:
@@ -281,14 +288,28 @@ class Identity(base.IdentityDriverBase):
with sql.session_for_read() as session:
self.get_group(group_id)
self.get_user(user_id)

# Note(knikolla): Check for normal group membership
query = session.query(model.UserGroupMembership)
query = query.filter_by(user_id=user_id)
query = query.filter_by(group_id=group_id)
if not query.first():
raise exception.NotFound(_("User '%(user_id)s' not found in"
" group '%(group_id)s'") %
{'user_id': user_id,
'group_id': group_id})
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"
" group '%(group_id)s'") %
{'user_id': user_id,
'group_id': group_id})

def remove_user_from_group(self, user_id, group_id):
# We don't check if user or group are still valid and let the remove
@@ -310,12 +331,33 @@ class Identity(base.IdentityDriverBase):
session.delete(membership_ref)

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:
self.get_user(user_id)
query = session.query(model.Group).join(model.UserGroupMembership)
query = query.filter(model.UserGroupMembership.user_id == user_id)
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):
with sql.session_for_read() as session:


+ 3
- 0
keystone/identity/core.py View File

@@ -1309,6 +1309,9 @@ class Manager(manager.Manager):
# driver selection, so remove any such filter
self._mark_domain_id_filter_satisfied(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(
ref_list, domain_id, driver, mapping.EntityType.GROUP)



+ 29
- 0
keystone/identity/shadow_backends/sql.py View File

@@ -195,3 +195,32 @@ class ShadowUsers(base.ShadowUsersDriverBase):
fed_user_refs = sql.filter_limit_query(model.FederatedUser, query,
hints)
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()
))

+ 2
- 0
keystone/tests/unit/test_backend_ldap.py View File

@@ -984,6 +984,8 @@ class BaseLDAPIdentity(LDAPTestSetup, IdentityTests, AssignmentTests,

# List groups for user.
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
self.assertThat(ref_list, matchers.Equals([group]))


+ 89
- 0
keystone/tests/unit/test_backend_sql.py View File

@@ -16,6 +16,7 @@ import datetime
from unittest import mock
import uuid

import freezegun
from oslo_db import exception as db_exception
from oslo_db import options
import sqlalchemy
@@ -671,6 +672,94 @@ class SqlIdentity(SqlTests,
negative_user['id'])
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):
"""Test the special storage of domain_id=None in sql resource driver.



Loading…
Cancel
Save