Skip to content

Commit

Permalink
Monotonic journals (#13936)
Browse files Browse the repository at this point in the history
  • Loading branch information
dstufft authored May 30, 2024
1 parent f325607 commit eabc81d
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 31 deletions.
6 changes: 0 additions & 6 deletions tests/unit/admin/views/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,12 +353,6 @@ def test_deletes_user(self, db_request, monkeypatch):
.one()
)
assert remove_journal.name == project.name
nuke_journal = (
db_request.db.query(JournalEntry)
.filter(JournalEntry.action == "nuke user")
.one()
)
assert nuke_journal.name == f"user:{user.username}"

def test_deletes_user_bad_confirm(self, db_request, monkeypatch):
user = UserFactory.create()
Expand Down
9 changes: 0 additions & 9 deletions tests/unit/utils/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,6 @@ def test_destroy_docs(db_request, flash):

destroy_docs(project, db_request, flash=flash)

journal_entry = (
db_request.db.query(JournalEntry)
.options(joinedload(JournalEntry.submitted_by))
.filter(JournalEntry.name == "foo")
.one()
)
assert journal_entry.action == "docdestroy"
assert journal_entry.submitted_by == db_request.user

assert not (
db_request.db.query(Project)
.filter(Project.name == project.name)
Expand Down
35 changes: 35 additions & 0 deletions warehouse/admin/views/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
from sqlalchemy.exc import NoResultFound
from sqlalchemy.orm import joinedload

from warehouse.accounts.interfaces import IUserService
from warehouse.accounts.models import User
from warehouse.authnz import Permissions
from warehouse.events.tags import EventTag
from warehouse.forklift.legacy import MAX_FILESIZE, MAX_PROJECT_SIZE
from warehouse.observations.models import OBSERVATION_KIND_MAP, ObservationKind
from warehouse.packaging.models import JournalEntry, Project, Release, Role
Expand Down Expand Up @@ -601,6 +603,27 @@ def add_role(project, request):

request.db.add(Role(role_name=role_name, user=user, project=project))

user_service = request.find_service(IUserService, context=None)

project.record_event(
tag=EventTag.Project.RoleAdd,
request=request,
additional={
"submitted_by": user_service.get_admin_user(),
"role_name": role_name,
"target_user": user.username,
},
)
user.record_event(
tag=EventTag.Account.RoleAdd,
request=request,
additional={
"submitted_by": user_service.get_admin_user(),
"project_name": project.name,
"role_name": role_name,
},
)

request.session.flash(
f"Added '{user.username}' as '{role_name}' on '{project.name}'", queue="success"
)
Expand Down Expand Up @@ -649,6 +672,18 @@ def delete_role(project, request):
)
)

user_service = request.find_service(IUserService, context=None)

project.record_event(
tag=EventTag.Project.RoleRemove,
request=request,
additional={
"submitted_by": user_service.get_admin_user(),
"role_name": role.role_name,
"target_user": role.user.username,
},
)

request.db.delete(role)

return HTTPSeeOther(
Expand Down
7 changes: 0 additions & 7 deletions warehouse/admin/views/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,13 +241,6 @@ def _nuke_user(user, request):

# Delete the user
request.db.delete(user)
request.db.add(
JournalEntry(
name=f"user:{user.username}",
action="nuke user",
submitted_by=request.user,
)
)


@view_config(
Expand Down
37 changes: 36 additions & 1 deletion warehouse/packaging/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,24 @@
FetchedValue,
ForeignKey,
Index,
Integer,
String,
Text,
UniqueConstraint,
cast,
func,
or_,
orm,
select,
sql,
)
from sqlalchemy.dialects.postgresql import ARRAY, CITEXT, ENUM, UUID as PG_UUID
from sqlalchemy.dialects.postgresql import (
ARRAY,
CITEXT,
ENUM,
REGCLASS,
UUID as PG_UUID,
)
from sqlalchemy.exc import MultipleResultsFound, NoResultFound
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.hybrid import hybrid_property
Expand Down Expand Up @@ -74,6 +83,8 @@
if typing.TYPE_CHECKING:
from warehouse.oidc.models import OIDCPublisher

_MONOTONIC_SEQUENCE = 42


class Role(db.Model):
__tablename__ = "roles"
Expand Down Expand Up @@ -860,6 +871,30 @@ def __table_args__(cls): # noqa
submitted_by: Mapped[User] = orm.relationship(lazy="raise_on_sql")


@db.listens_for(JournalEntry, "before_insert")
def ensure_monotonic_journals(config, mapper, connection, target):
# We rely on `journals.id` to be a monotonically increasing integer,
# however the way that SERIAL is implemented, it does not guarentee
# that is the case.
#
# Ultimately SERIAL fetches the next integer regardless of what happens
# inside of the transaction. So journals.id will get filled in, in order
# of when the `INSERT` statements were executed, but not in the order
# that transactions were committed.
#
# The way this works, not even the SERIALIZABLE transaction types give
# us this property. Instead we have to implement our own locking that
# ensures that each new journal entry will be serialized.
connection.execute(
select(
func.pg_advisory_xact_lock(
cast(cast(target.__tablename__, REGCLASS), Integer),
_MONOTONIC_SEQUENCE,
)
)
)


class ProhibitedProjectName(db.Model):
__tablename__ = "prohibited_project_names"
__table_args__ = (
Expand Down
8 changes: 0 additions & 8 deletions warehouse/utils/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,14 +85,6 @@ def remove_project(project, request, flash=True):


def destroy_docs(project, request, flash=True):
request.db.add(
JournalEntry(
name=project.name,
action="docdestroy",
submitted_by=request.user,
)
)

request.task(remove_documentation).delay(project.name)

project.has_docs = False
Expand Down

0 comments on commit eabc81d

Please sign in to comment.