fix: set transaction user based on session info, when applicable

combined with `--runas` CLI param, this gives "any" command a way to
assign authorship for versioning
This commit is contained in:
Lance Edgar 2025-12-29 10:41:09 -06:00
parent 1fc280eff1
commit d22a9963bf
2 changed files with 61 additions and 27 deletions

View file

@ -80,6 +80,9 @@ class WuttaContinuumConfigExtension(WuttaConfigExtension):
[wutta_continuum]
wutta_plugin_spec = poser.db.continuum:PoserContinuumPlugin
See also the SQLAlchemy-Continuum docs for
:doc:`sqlalchemy-continuum:plugins`.
"""
# only do this if config enables it
if not config.get_bool(
@ -113,44 +116,64 @@ class WuttaContinuumPlugin(Plugin):
"""
SQLAlchemy-Continuum manager plugin for Wutta-Continuum.
This tries to assign the current user and IP address to the
transaction.
This is the default plugin used within
:meth:`~WuttaContinuumConfigExtension.startup()` unless config
overrides.
It will assume the "current machine" IP address, which may be
suitable for some apps but not all (e.g. web apps, where IP
address should reflect an arbitrary client machine).
However it does not actually have a way to determine the current
user. WuttaWeb therefore uses a different plugin, based on this
one, to get both the user and IP address from current request.
You can override this to use a custom plugin for this purpose; if
so you must specify in your config file:
.. code-block:: ini
[wutta_continuum]
wutta_plugin_spec = poser.db.continuum:PoserContinuumPlugin
This tries to establish the user and IP address responsible, and
comment if applicable, for the current transaction.
See also the SQLAlchemy-Continuum docs for
:doc:`sqlalchemy-continuum:plugins`.
"""
def get_remote_addr( # pylint: disable=empty-docstring,unused-argument
self, uow, session
):
""" """
def get_remote_addr(self, uow, session): # pylint: disable=unused-argument
"""
This should return the effective IP address responsible for
the current change(s).
Default logic will assume the "current machine" e.g. where a
CLI command or script is running. In practice that often
means this winds up being ``127.0.0.1`` or similar.
:returns: IP address (v4 or v6) as string
"""
host = socket.gethostname()
return socket.gethostbyname(host)
def get_user_id( # pylint: disable=empty-docstring,unused-argument
self, uow, session
):
""" """
def get_user_id(self, uow, session): # pylint: disable=unused-argument
"""
This should return the effective ``User.uuid`` indicating who
is responsible for the current change(s).
Default logic does not have a way to determine current user on
its own per se. However it can inspect the session, and use a
value from there if found.
Any session can therefore declare the resonsible user::
myuser = session.query(model.User).first()
session.info["continuum_user_id"] = myuser.uuid
:returns: :attr:`wuttjamaican.db.model.auth.User.uuid` value,
or ``None``
"""
if user_id := session.info.get("continuum_user_id"):
return user_id
return None
def transaction_args(self, uow, session): # pylint: disable=empty-docstring
""" """
def transaction_args(self, uow, session):
"""
This is a standard hook method for SQLAchemy-Continuum
plugins. We use it to (try to) inject these values, which
then become set on the current (new) transaction:
* ``remote_addr`` - effective IP address causing the change
* see :meth:`get_remote_addr()`
* ``user_id`` - effective ``User.uuid`` for change authorship
* see :meth:`get_user_id()`
"""
kwargs = {}
remote_addr = self.get_remote_addr(uow, session)

View file

@ -55,9 +55,20 @@ class TestWuttaContinuumPlugin(DataTestCase):
self.assertEqual(plugin.get_remote_addr(None, self.session), "127.0.0.1")
def test_user_id(self):
model = self.app.model
plugin = self.make_plugin()
fred = model.User(username="fred")
self.session.add(fred)
self.session.commit()
# empty by default
self.assertIsNone(plugin.get_user_id(None, self.session))
# but session can declare one
self.session.info["continuum_user_id"] = fred.uuid
self.assertEqual(plugin.get_user_id(None, self.session), fred.uuid)
def test_transaction_args(self):
plugin = self.make_plugin()
with patch.object(socket, "gethostbyname", return_value="127.0.0.1"):