Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backend handling of setting user-role, user-group, and group-role associations #18777

Merged
merged 44 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
e25f5e0
Add directory for migrations data fixing scripts
jdavcs Sep 18, 2024
505cd87
Add username deduplication data fixer
jdavcs Sep 18, 2024
90f298b
Add migration for username column unique constraint
jdavcs Jul 3, 2024
0e4559f
Add email deduplication data fixer
jdavcs Sep 18, 2024
bacc304
Add migration for email column unique constraint
jdavcs Jul 3, 2024
07c0b2d
Update the model w/unique constraints
jdavcs Jul 4, 2024
527b371
Fix integration test that violated db integrity constraint
jdavcs Jul 4, 2024
b261dea
Randomize test user email to respect unique constraint
jdavcs Sep 19, 2024
d8abcad
Fix bug in test_rule_helper
jdavcs Sep 19, 2024
11a25ae
Fix test_quota to respect constraint
jdavcs Sep 19, 2024
600523b
Fix test_galaxy_mapping to respect constraint
jdavcs Sep 19, 2024
4471c6a
Refactor tests to reduce duplication
jdavcs Sep 21, 2024
87573ee
Add tests for migration data fixes
jdavcs Sep 21, 2024
bef3888
Refactor for testability: pass only session
jdavcs Aug 13, 2024
7739134
Add unit tests for user/group/role associations
jdavcs Aug 27, 2024
004d46c
Fix test database handling for db tests
jdavcs Aug 16, 2024
4fb1c70
Rename verify_items helper to have_same_elements
jdavcs Aug 26, 2024
99afd7b
Refactor setting user/group/role associations
jdavcs Aug 28, 2024
b9ac9f7
Do not import psycopg2; test for error code instead
jdavcs Aug 29, 2024
6c2125a
Drop associate_components method (no longer used)
jdavcs Aug 29, 2024
356b8c3
Add scripts to fix association data
jdavcs Sep 18, 2024
b89c208
Alter model for user-role-association
jdavcs Aug 26, 2024
0607d2e
Add migration: unique constraint for user-role-assoc
jdavcs Sep 23, 2024
da6a7a7
Add migration: not null constraint for user-role-assoc
jdavcs Sep 10, 2024
4ce44cf
Alter model for user-group-assoc
jdavcs Aug 28, 2024
332550a
Add migration: unique constraint for user-group-assoc
jdavcs Sep 10, 2024
cfaef6f
Add migration: not null constraint for user-group-assoc
jdavcs Sep 10, 2024
b4ebfca
Alter model for group-role-assoc
jdavcs Aug 28, 2024
8900779
Add migration: unique constraint for group-role-association
jdavcs Sep 10, 2024
49485c8
Add migration: not null constraint for group-role-assoc
jdavcs Sep 10, 2024
19d76e9
Fix mypy: statements are reachable
jdavcs Aug 29, 2024
f6763a5
Fix api test: pass full set of new associations
jdavcs Aug 30, 2024
4b95955
Drop unused non_private_roles attr from model
jdavcs Sep 1, 2024
8c4a900
Add tests for user and history default permissions
jdavcs Sep 1, 2024
6c4ddeb
Test that private roles are not assignable
jdavcs Sep 3, 2024
ef9a5db
Remove "or []"
jdavcs Sep 10, 2024
552354c
Remove "or []"
jdavcs Sep 10, 2024
e839b39
Remove last "or []"
jdavcs Sep 10, 2024
175423c
Give method more appropriate name
jdavcs Sep 10, 2024
e3e8856
Replace loop with sinle select statement
jdavcs Sep 10, 2024
2fc21f9
Do not pass unnecessary arguments
jdavcs Sep 12, 2024
a509bf8
Fix mypy
jdavcs Sep 23, 2024
5524ea7
Add tests for migration fixes
jdavcs Sep 24, 2024
5d5065a
Update API schema for Group model
jdavcs Sep 24, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/galaxy/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -655,7 +655,7 @@ def __init__(self, configure_logging=True, use_converters=True, use_display_appl
# Load security policy.
self.security_agent = self.model.security_agent
self.host_security_agent = galaxy.model.security.HostAgent(
model=self.security_agent.model, permitted_actions=self.security_agent.permitted_actions
self.security_agent.sa_session, permitted_actions=self.security_agent.permitted_actions
)

# We need the datatype registry for running certain tasks that modify HDAs, and to build the registry we need
Expand Down
31 changes: 8 additions & 23 deletions lib/galaxy/managers/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
from galaxy.managers.context import ProvidesAppContext
from galaxy.model import Group
from galaxy.model.base import transaction
from galaxy.model.db.role import get_roles_by_ids
from galaxy.model.db.user import get_users_by_ids
from galaxy.model.scoped_session import galaxy_scoped_session
from galaxy.schema.fields import Security
from galaxy.schema.groups import (
Expand Down Expand Up @@ -54,13 +52,11 @@ def create(self, trans: ProvidesAppContext, payload: GroupCreatePayload):

group = model.Group(name=name)
sa_session.add(group)
user_ids = payload.user_ids
users = get_users_by_ids(sa_session, user_ids)
role_ids = payload.role_ids
roles = get_roles_by_ids(sa_session, role_ids)
trans.app.security_agent.set_entity_group_associations(groups=[group], roles=roles, users=users)
with transaction(sa_session):
sa_session.commit()

trans.app.security_agent.set_group_user_and_role_associations(
group, user_ids=payload.user_ids, role_ids=payload.role_ids
)
sa_session.commit()

encoded_id = Security.security.encode_id(group.id)
item = group.to_dict(view="element")
Expand Down Expand Up @@ -88,23 +84,12 @@ def update(self, trans: ProvidesAppContext, group_id: int, payload: GroupUpdateP
if name := payload.name:
self._check_duplicated_group_name(sa_session, name)
group.name = name
sa_session.add(group)

users = None
if payload.user_ids is not None:
users = get_users_by_ids(sa_session, payload.user_ids)

roles = None
if payload.role_ids is not None:
roles = get_roles_by_ids(sa_session, payload.role_ids)
sa_session.commit()

self._app.security_agent.set_entity_group_associations(
groups=[group], roles=roles, users=users, delete_existing_assocs=False
self._app.security_agent.set_group_user_and_role_associations(
group, user_ids=payload.user_ids, role_ids=payload.role_ids
)

with transaction(sa_session):
sa_session.commit()

encoded_id = Security.security.encode_id(group.id)
item = group.to_dict(view="element")
item["url"] = self._url_for(trans, "show_group", group_id=encoded_id)
Expand Down
25 changes: 10 additions & 15 deletions lib/galaxy/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,7 @@ class User(Base, Dictifiable, RepresentById):
id: Mapped[int] = mapped_column(primary_key=True)
create_time: Mapped[datetime] = mapped_column(default=now, nullable=True)
update_time: Mapped[datetime] = mapped_column(default=now, onupdate=now, nullable=True)
email: Mapped[str] = mapped_column(TrimmedString(255), index=True)
email: Mapped[str] = mapped_column(TrimmedString(255), index=True, unique=True)
username: Mapped[Optional[str]] = mapped_column(TrimmedString(255), index=True, unique=True)
password: Mapped[str] = mapped_column(TrimmedString(255))
last_password_change: Mapped[Optional[datetime]] = mapped_column(default=now)
Expand Down Expand Up @@ -849,14 +849,6 @@ class User(Base, Dictifiable, RepresentById):
all_notifications: Mapped[List["UserNotificationAssociation"]] = relationship(
back_populates="user", cascade_backrefs=False
)
non_private_roles: Mapped[List["UserRoleAssociation"]] = relationship(
viewonly=True,
primaryjoin=(
lambda: (User.id == UserRoleAssociation.user_id)
& (UserRoleAssociation.role_id == Role.id)
& not_(Role.name == User.email)
),
)

preferences: AssociationProxy[Any]

Expand Down Expand Up @@ -2967,10 +2959,11 @@ def __init__(self, name=None):

class UserGroupAssociation(Base, RepresentById):
__tablename__ = "user_group_association"
__table_args__ = (UniqueConstraint("user_id", "group_id"),)

id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("galaxy_user.id"), index=True, nullable=True)
group_id: Mapped[int] = mapped_column(ForeignKey("galaxy_group.id"), index=True, nullable=True)
user_id: Mapped[int] = mapped_column(ForeignKey("galaxy_user.id"), index=True)
group_id: Mapped[int] = mapped_column(ForeignKey("galaxy_group.id"), index=True)
create_time: Mapped[datetime] = mapped_column(default=now, nullable=True)
update_time: Mapped[datetime] = mapped_column(default=now, onupdate=now, nullable=True)
user: Mapped["User"] = relationship(back_populates="groups")
Expand Down Expand Up @@ -3685,10 +3678,11 @@ class HistoryUserShareAssociation(Base, UserShareAssociation):

class UserRoleAssociation(Base, RepresentById):
__tablename__ = "user_role_association"
__table_args__ = (UniqueConstraint("user_id", "role_id"),)

id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("galaxy_user.id"), index=True, nullable=True)
role_id: Mapped[int] = mapped_column(ForeignKey("role.id"), index=True, nullable=True)
user_id: Mapped[int] = mapped_column(ForeignKey("galaxy_user.id"), index=True)
role_id: Mapped[int] = mapped_column(ForeignKey("role.id"), index=True)
create_time: Mapped[datetime] = mapped_column(default=now, nullable=True)
update_time: Mapped[datetime] = mapped_column(default=now, onupdate=now, nullable=True)

Expand All @@ -3703,10 +3697,11 @@ def __init__(self, user, role):

class GroupRoleAssociation(Base, RepresentById):
__tablename__ = "group_role_association"
__table_args__ = (UniqueConstraint("group_id", "role_id"),)

id: Mapped[int] = mapped_column(primary_key=True)
group_id: Mapped[int] = mapped_column(ForeignKey("galaxy_group.id"), index=True, nullable=True)
role_id: Mapped[int] = mapped_column(ForeignKey("role.id"), index=True, nullable=True)
group_id: Mapped[int] = mapped_column(ForeignKey("galaxy_group.id"), index=True)
role_id: Mapped[int] = mapped_column(ForeignKey("role.id"), index=True)
create_time: Mapped[datetime] = mapped_column(default=now, nullable=True)
update_time: Mapped[datetime] = mapped_column(default=now, onupdate=now, nullable=True)
group: Mapped["Group"] = relationship(back_populates="roles")
Expand Down
2 changes: 1 addition & 1 deletion lib/galaxy/model/mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ def _build_model_mapping(engine, map_install_models, thread_local_log) -> Galaxy
model_modules.append(tool_shed_install)

model_mapping = GalaxyModelMapping(model_modules, engine)
model_mapping.security_agent = GalaxyRBACAgent(model_mapping)
model_mapping.security_agent = GalaxyRBACAgent(model_mapping.session)
model_mapping.thread_local_log = thread_local_log
return model_mapping

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Add not-null constraints to user_group_association

Revision ID: 13fe10b8e35b
Revises: 56ddf316dbd0
Create Date: 2024-09-09 21:26:26.032842

"""

from alembic import op

from galaxy.model.migrations.data_fixes.association_table_fixer import UserGroupAssociationNullFix
from galaxy.model.migrations.util import (
alter_column,
transaction,
)

# revision identifiers, used by Alembic.
revision = "13fe10b8e35b"
down_revision = "56ddf316dbd0"
branch_labels = None
depends_on = None

table_name = "user_group_association"


def upgrade():
with transaction():
_remove_records_with_nulls()
alter_column(table_name, "user_id", nullable=False)
alter_column(table_name, "group_id", nullable=False)


def downgrade():
with transaction():
alter_column(table_name, "user_id", nullable=True)
alter_column(table_name, "group_id", nullable=True)


def _remove_records_with_nulls():
"""Remove associations having null as user_id or group_id"""
connection = op.get_bind()
UserGroupAssociationNullFix(connection).run()
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Email column unique constraint

Revision ID: 1cf595475b58
Revises: d619fdfa6168
Create Date: 2024-07-03 19:53:22.443016
"""

from alembic import op

from galaxy.model.database_object_names import build_index_name
from galaxy.model.migrations.data_fixes.user_table_fixer import EmailDeduplicator
from galaxy.model.migrations.util import (
create_index,
drop_index,
index_exists,
transaction,
)

# revision identifiers, used by Alembic.
revision = "1cf595475b58"
down_revision = "d619fdfa6168"
branch_labels = None
depends_on = None


table_name = "galaxy_user"
column_name = "email"
index_name = build_index_name(table_name, [column_name])


def upgrade():
with transaction():
_fix_duplicate_emails()
# Existing databases may have an existing index we no longer need
# New databases will not have that index, so we must check.
if index_exists(index_name, table_name, False):
drop_index(index_name, table_name)
# Create a UNIQUE index
create_index(index_name, table_name, [column_name], unique=True)


def downgrade():
with transaction():
drop_index(index_name, table_name)
# Restore a non-unique index
create_index(index_name, table_name, [column_name])


def _fix_duplicate_emails():
"""Fix records with duplicate usernames"""
connection = op.get_bind()
EmailDeduplicator(connection).run()
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Add not-null constraints to user_role_association

Revision ID: 1fdd615f2cdb
Revises: 349dd9d9aac9
Create Date: 2024-09-09 21:28:11.987054

"""

from alembic import op

from galaxy.model.migrations.data_fixes.association_table_fixer import UserRoleAssociationNullFix
from galaxy.model.migrations.util import (
alter_column,
transaction,
)

# revision identifiers, used by Alembic.
revision = "1fdd615f2cdb"
down_revision = "349dd9d9aac9"
branch_labels = None
depends_on = None

table_name = "user_role_association"


def upgrade():
with transaction():
_remove_records_with_nulls()
alter_column(table_name, "user_id", nullable=False)
alter_column(table_name, "role_id", nullable=False)


def downgrade():
with transaction():
alter_column(table_name, "user_id", nullable=True)
alter_column(table_name, "role_id", nullable=True)


def _remove_records_with_nulls():
"""Remove associations having null as user_id or role_id"""
connection = op.get_bind()
UserRoleAssociationNullFix(connection).run()
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Add not-null constraints to group_role_association

Revision ID: 25b092f7938b
Revises: 9ef6431f3a4e
Create Date: 2024-09-09 16:17:26.652865

"""

from alembic import op

from galaxy.model.migrations.data_fixes.association_table_fixer import GroupRoleAssociationNullFix
from galaxy.model.migrations.util import (
alter_column,
transaction,
)

# revision identifiers, used by Alembic.
revision = "25b092f7938b"
down_revision = "9ef6431f3a4e"
branch_labels = None
depends_on = None

table_name = "group_role_association"


def upgrade():
with transaction():
_remove_records_with_nulls()
alter_column(table_name, "group_id", nullable=True)
alter_column(table_name, "role_id", nullable=False)


def downgrade():
with transaction():
alter_column(table_name, "group_id", nullable=True)
alter_column(table_name, "role_id", nullable=True)


def _remove_records_with_nulls():
"""Remove associations having null as group_id or role_id"""
connection = op.get_bind()
GroupRoleAssociationNullFix(connection).run()
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Add unique constraint to user_role_association

Revision ID: 349dd9d9aac9
Revises: 1cf595475b58
Create Date: 2024-09-09 16:14:58.278850

"""

from alembic import op

from galaxy.model.migrations.data_fixes.association_table_fixer import UserRoleAssociationDuplicateFix
from galaxy.model.migrations.util import (
create_unique_constraint,
drop_constraint,
transaction,
)

# revision identifiers, used by Alembic.
revision = "349dd9d9aac9"
down_revision = "1cf595475b58"
branch_labels = None
depends_on = None

table_name = "user_role_association"
constraint_column_names = ["user_id", "role_id"]
unique_constraint_name = (
"user_role_association_user_id_key" # This is what the model's naming convention will generate.
)


def upgrade():
with transaction():
_remove_duplicate_records()
create_unique_constraint(unique_constraint_name, table_name, constraint_column_names)


def downgrade():
with transaction():
drop_constraint(unique_constraint_name, table_name)


def _remove_duplicate_records():
"""Remove duplicate associations"""
connection = op.get_bind()
UserRoleAssociationDuplicateFix(connection).run()
Loading
Loading