diff --git a/warehouse/accounts/views.py b/warehouse/accounts/views.py index 139976739111..c8478bed1ed1 100644 --- a/warehouse/accounts/views.py +++ b/warehouse/accounts/views.py @@ -1252,7 +1252,8 @@ def _error(message): request.db.add(Role(user=user, project=project, role_name=desired_role)) request.db.delete(role_invite) request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + request.db, name=project.name, action=f"accepted {desired_role} {user.username}", submitted_by=request.user, diff --git a/warehouse/admin/views/projects.py b/warehouse/admin/views/projects.py index b536586ac500..59ce1b49037b 100644 --- a/warehouse/admin/views/projects.py +++ b/warehouse/admin/views/projects.py @@ -592,7 +592,8 @@ def add_role(project, request): ) request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + request.db, name=project.name, action=f"add {role_name} {user.username}", submitted_by=request.user, @@ -642,7 +643,8 @@ def delete_role(project, request): queue="success", ) request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + request.db, name=project.name, action=f"remove {role.role_name} {role.user.username}", submitted_by=request.user, diff --git a/warehouse/admin/views/users.py b/warehouse/admin/views/users.py index f58a0b3d784e..1302db69832e 100644 --- a/warehouse/admin/views/users.py +++ b/warehouse/admin/views/users.py @@ -211,7 +211,8 @@ def _nuke_user(user, request): ) for project in projects: request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + request.db, name=project.name, action="remove project", submitted_by=request.user, @@ -242,7 +243,8 @@ def _nuke_user(user, request): # Delete the user request.db.delete(user) request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + request.db, name=f"user:{user.username}", action="nuke user", submitted_by=request.user, diff --git a/warehouse/forklift/legacy.py b/warehouse/forklift/legacy.py index e925db39489c..e19231e90d59 100644 --- a/warehouse/forklift/legacy.py +++ b/warehouse/forklift/legacy.py @@ -773,7 +773,8 @@ def file_upload(request): # a SQLAlchemy hook or the like instead of doing it inline in # this view. request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + request.db, name=release.project.name, version=release.version, action="new release", @@ -1123,7 +1124,8 @@ def file_upload(request): # SQLAlchemy hook or the like instead of doing it inline in this # view. request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + request.db, name=release.project.name, version=release.version, action="add {python_version} file {filename}".format( diff --git a/warehouse/manage/views/__init__.py b/warehouse/manage/views/__init__.py index ca93ef799313..bedf17b661f0 100644 --- a/warehouse/manage/views/__init__.py +++ b/warehouse/manage/views/__init__.py @@ -1992,7 +1992,8 @@ def yank_project_release(self): ) self.request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + self.request.db, name=self.release.project.name, action="yank release", version=self.release.version, @@ -2077,7 +2078,8 @@ def unyank_project_release(self): ) self.request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + self.request.db, name=self.release.project.name, action="unyank release", version=self.release.version, @@ -2178,7 +2180,8 @@ def delete_project_release(self): ) self.request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + self.request.db, name=self.release.project.name, action="remove release", version=self.release.version, @@ -2270,7 +2273,8 @@ def _error(message): ) self.request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + self.request.db, name=self.release.project.name, action=f"remove file {release_file.filename}", version=self.release.version, @@ -2429,7 +2433,8 @@ def manage_project_roles(project, request, _form_class=CreateRoleForm): # Add journal entry. request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + request.db, name=project.name, action=f"add {role_name.value} {team_name}", submitted_by=request.user, @@ -2539,7 +2544,8 @@ def manage_project_roles(project, request, _form_class=CreateRoleForm): # Add journal entry. request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + request.db, name=project.name, action=f"add {role_name} {user.username}", submitted_by=request.user, @@ -2663,7 +2669,8 @@ def manage_project_roles(project, request, _form_class=CreateRoleForm): ) request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + request.db, name=project.name, action=f"invite {role_name} {username}", submitted_by=request.user, @@ -2748,7 +2755,8 @@ def revoke_project_role_invitation(project, request, _form_class=ChangeRoleForm) role_name = token_data.get("desired_role") request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + request.db, name=project.name, action=f"revoke_invite {role_name} {user.username}", submitted_by=request.user, @@ -2810,7 +2818,8 @@ def change_project_role(project, request, _form_class=ChangeRoleForm): request.session.flash("Cannot remove yourself as Owner", queue="error") else: request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + request.db, name=project.name, action="change {} {} to {}".format( role.role_name, role.user.username, form.role_name.data @@ -2896,7 +2905,8 @@ def delete_project_role(project, request): else: request.db.delete(role) request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + request.db, name=project.name, action=f"remove {role.role_name} {role.user.username}", submitted_by=request.user, diff --git a/warehouse/manage/views/organizations.py b/warehouse/manage/views/organizations.py index 5e618526707d..d04c1a3bb0c7 100644 --- a/warehouse/manage/views/organizations.py +++ b/warehouse/manage/views/organizations.py @@ -745,7 +745,8 @@ def add_organization_project(self): if role: self.request.db.delete(role) self.request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + self.request.db, name=project.name, action=f"remove {role.role_name} {role.user.username}", submitted_by=self.request.user, @@ -1521,7 +1522,8 @@ def transfer_organization_project(project, request): if role: request.db.delete(role) request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + request.db, name=project.name, action=f"remove {role.role_name} {role.user.username}", submitted_by=request.user, diff --git a/warehouse/manage/views/teams.py b/warehouse/manage/views/teams.py index 3f429fc1acf0..a4d18180adc3 100644 --- a/warehouse/manage/views/teams.py +++ b/warehouse/manage/views/teams.py @@ -521,7 +521,8 @@ def change_team_project_role(project, request, _form_class=ChangeTeamProjectRole else: # Add journal entry. request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + request.db, name=project.name, action="change {} {} to {}".format( role.role_name.value, @@ -630,7 +631,8 @@ def delete_team_project_role(project, request): # Add journal entry. request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + request.db, name=project.name, action=f"remove {role_name.value} {team.name}", submitted_by=request.user, diff --git a/warehouse/packaging/models.py b/warehouse/packaging/models.py index dd4ddca38c68..10ac72104803 100644 --- a/warehouse/packaging/models.py +++ b/warehouse/packaging/models.py @@ -12,6 +12,7 @@ from __future__ import annotations import enum +import hashlib import typing from collections import OrderedDict @@ -32,9 +33,11 @@ String, Text, UniqueConstraint, + cast, func, or_, orm, + select, sql, ) from sqlalchemy.dialects.postgresql import ARRAY, CITEXT, ENUM, UUID as PG_UUID @@ -859,6 +862,14 @@ def __table_args__(cls): # noqa ) submitted_by: Mapped[User] = orm.relationship(lazy="raise_on_sql") + @classmethod + def create_with_lock(cls, session, *args, **kwargs): + hashed = hashlib.blake2b(b"table:journals").digest()[:8] + key = int.from_bytes(hashed, "little", signed=True) + session.execute(select(func.pg_advisory_xact_lock(cast(key, BigInteger)))) + + return cls(*args, **kwargs) + class ProhibitedProjectName(db.Model): __tablename__ = "prohibited_project_names" diff --git a/warehouse/packaging/services.py b/warehouse/packaging/services.py index bece370cf0eb..e327d397c899 100644 --- a/warehouse/packaging/services.py +++ b/warehouse/packaging/services.py @@ -547,7 +547,8 @@ def create_project( # SQLAlchemy hook or the like instead of doing it inline in this # service. self.db.add( - JournalEntry( + JournalEntry.create_with_lock( + request.db, name=project.name, action="create", submitted_by=creator, @@ -566,7 +567,8 @@ def create_project( # SQLAlchemy hook or the like instead of doing it inline in this # service. self.db.add( - JournalEntry( + JournalEntry.create_with_lock( + request.db, name=project.name, action=f"add Owner {creator.username}", submitted_by=creator, diff --git a/warehouse/utils/project.py b/warehouse/utils/project.py index 447628c6d912..386d9806e3aa 100644 --- a/warehouse/utils/project.py +++ b/warehouse/utils/project.py @@ -70,7 +70,8 @@ def remove_project(project, request, flash=True): # some kind of garbage collection at some point. request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + request.db, name=project.name, action="remove project", submitted_by=request.user, @@ -86,7 +87,8 @@ def remove_project(project, request, flash=True): def destroy_docs(project, request, flash=True): request.db.add( - JournalEntry( + JournalEntry.create_with_lock( + request.db, name=project.name, action="docdestroy", submitted_by=request.user,