Expiring User Group Membership Model

Creates the model and migration for the expiring user group
membership table.

Change-Id: I48093403539918f81e6a174bdfa7b6497dd307fb
Partial-Bug: 1809116
This commit is contained in:
Kristi Nikolla 2019-07-29 16:19:51 -04:00
parent ba2e4b83e8
commit ee54ba0ce4
8 changed files with 163 additions and 3 deletions

View File

@ -0,0 +1,15 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
def upgrade(migrate_engine):
pass

View File

@ -0,0 +1,15 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
def upgrade(migrate_engine):
pass

View File

@ -0,0 +1,47 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import sqlalchemy as sql
def upgrade(migrate_engine):
meta = sql.MetaData()
meta.bind = migrate_engine
identity_provider = sql.Table('identity_provider', meta, autoload=True)
authorization_ttl = sql.Column('authorization_ttl', sql.Integer,
nullable=True)
identity_provider.create_column(authorization_ttl)
user_table = sql.Table('user', meta, autoload=True)
group_table = sql.Table('group', meta, autoload=True)
idp_table = sql.Table('identity_provider', meta, autoload=True)
expiring_user_group_membership = sql.Table(
'expiring_user_group_membership', meta,
sql.Column('user_id', sql.String(64),
sql.ForeignKey(user_table.c.id), primary_key=True),
sql.Column('group_id', sql.String(64),
sql.ForeignKey(group_table.c.id), primary_key=True),
sql.Column('idp_id',
sql.String(64),
sql.ForeignKey(idp_table.c.id,
ondelete='CASCADE'),
primary_key=True),
sql.Column('last_verified', sql.DateTime(), nullable=False),
mysql_engine='InnoDB',
mysql_charset='utf8'
)
expiring_user_group_membership.create(migrate_engine, checkfirst=True)

View File

@ -94,6 +94,15 @@ enabled. There is typically no reason to disable this.
"""))
default_authorization_ttl = cfg.IntOpt(
'default_authorization_ttl',
default=0,
help=utils.fmt("""
Default time in minutes for the validity of group memberships carried over
from a mapping. Default is 0, which means disabled.
"""))
GROUP_NAME = __name__.split('.')[-1]
ALL_OPTS = [
driver,
@ -103,6 +112,7 @@ ALL_OPTS = [
trusted_dashboard,
sso_callback_template,
caching,
default_authorization_ttl,
]

View File

@ -51,16 +51,25 @@ class FederationProtocolModel(sql.ModelBase, sql.ModelDictMixin):
class IdentityProviderModel(sql.ModelBase, sql.ModelDictMixin):
__tablename__ = 'identity_provider'
attributes = ['id', 'domain_id', 'enabled', 'description', 'remote_ids']
mutable_attributes = frozenset(['description', 'enabled', 'remote_ids'])
attributes = ['id', 'domain_id', 'enabled', 'description', 'remote_ids',
'authorization_ttl']
mutable_attributes = frozenset(['description', 'enabled', 'remote_ids',
'authorization_ttl'])
id = sql.Column(sql.String(64), primary_key=True)
domain_id = sql.Column(sql.String(64), nullable=False)
enabled = sql.Column(sql.Boolean, nullable=False)
description = sql.Column(sql.Text(), nullable=True)
authorization_ttl = sql.Column(sql.Integer, nullable=True)
remote_ids = orm.relationship('IdPRemoteIdsModel',
order_by='IdPRemoteIdsModel.remote_id',
cascade='all, delete-orphan')
expiring_user_group_memberships = orm.relationship(
'ExpiringUserGroupMembership',
cascade='all, delete-orphan',
backref="idp"
)
@classmethod
def from_dict(cls, dictionary):

View File

@ -61,6 +61,11 @@ class User(sql.ModelBase, sql.ModelDictMixinWithExtras):
lazy='joined',
cascade='all,delete-orphan',
backref='user')
expiring_user_group_memberships = orm.relationship(
'ExpiringUserGroupMembership',
cascade='all, delete-orphan',
backref="user"
)
created_at = sql.Column(sql.DateTime, nullable=True)
last_active_at = sql.Column(sql.Date, nullable=True)
# unique constraint needed here to support composite fk constraints
@ -370,6 +375,11 @@ class Group(sql.ModelBase, sql.ModelDictMixinWithExtras):
domain_id = sql.Column(sql.String(64), nullable=False)
description = sql.Column(sql.Text())
extra = sql.Column(sql.JsonBlob())
expiring_user_group_memberships = orm.relationship(
'ExpiringUserGroupMembership',
cascade='all, delete-orphan',
backref="group"
)
# Unique constraint across two columns to create the separation
# rather than just only 'name' being unique
__table_args__ = (sql.UniqueConstraint('domain_id', 'name'),)
@ -387,6 +397,34 @@ class UserGroupMembership(sql.ModelBase, sql.ModelDictMixin):
primary_key=True)
class ExpiringUserGroupMembership(sql.ModelBase, sql.ModelDictMixin):
"""Expiring group membership through federation mapping rules."""
__tablename__ = 'expiring_user_group_membership'
user_id = sql.Column(sql.String(64),
sql.ForeignKey('user.id'),
primary_key=True)
group_id = sql.Column(sql.String(64),
sql.ForeignKey('group.id'),
primary_key=True)
idp_id = sql.Column(sql.String(64),
sql.ForeignKey('identity_provider.id',
ondelete='CASCADE'),
primary_key=True)
last_verified = sql.Column(sql.DateTime, nullable=False)
@hybrid_property
def expires(self):
ttl = self.idp.authorization_ttl
if not ttl:
ttl = CONF.federation.default_authorization_ttl
return self.last_verified + datetime.timedelta(minutes=ttl)
@hybrid_property
def expired(self):
return self.expires <= datetime.datetime.utcnow()
class UserOption(sql.ModelBase):
__tablename__ = 'user_option'
user_id = sql.Column(sql.String(64), sql.ForeignKey('user.id',

View File

@ -23,7 +23,8 @@ class SqlFederation(test_backend_sql.SqlModels):
cols = (('id', sql.String, 64),
('domain_id', sql.String, 64),
('enabled', sql.Boolean, None),
('description', sql.Text, None))
('description', sql.Text, None),
('authorization_ttl', sql.Integer, None))
self.assertExpectedSchema('identity_provider', cols)
def test_idp_remote_ids(self):

View File

@ -3474,6 +3474,31 @@ class FullMigration(SqlMigrateBase, unit.TestCase):
self.assertFalse(self.does_fk_exist('user', 'domain_id'))
self.assertFalse(self.does_fk_exist('identity_provider', 'domain_id'))
def test_migration_073_contract_expiring_group_membership(self):
self.expand(72)
self.migrate(72)
self.contract(72)
membership_table = 'expiring_user_group_membership'
self.assertTableDoesNotExist(membership_table)
idp_table = 'identity_provider'
self.assertTableColumns(
idp_table,
['id', 'domain_id', 'enabled', 'description'])
self.expand(73)
self.migrate(73)
self.contract(73)
self.assertTableColumns(
membership_table,
['user_id', 'group_id', 'idp_id', 'last_verified'])
self.assertTableColumns(
idp_table,
['id', 'domain_id', 'enabled', 'description',
'authorization_ttl'])
class MySQLOpportunisticFullMigration(FullMigration):
FIXTURE = db_fixtures.MySQLOpportunisticFixture