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_continuum]
wutta_plugin_spec = poser.db.continuum:PoserContinuumPlugin 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 # only do this if config enables it
if not config.get_bool( if not config.get_bool(
@ -113,44 +116,64 @@ class WuttaContinuumPlugin(Plugin):
""" """
SQLAlchemy-Continuum manager plugin for Wutta-Continuum. SQLAlchemy-Continuum manager plugin for Wutta-Continuum.
This tries to assign the current user and IP address to the This is the default plugin used within
transaction. :meth:`~WuttaContinuumConfigExtension.startup()` unless config
overrides.
It will assume the "current machine" IP address, which may be This tries to establish the user and IP address responsible, and
suitable for some apps but not all (e.g. web apps, where IP comment if applicable, for the current transaction.
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
See also the SQLAlchemy-Continuum docs for See also the SQLAlchemy-Continuum docs for
:doc:`sqlalchemy-continuum:plugins`. :doc:`sqlalchemy-continuum:plugins`.
""" """
def get_remote_addr( # pylint: disable=empty-docstring,unused-argument def get_remote_addr(self, uow, session): # pylint: disable=unused-argument
self, uow, session """
): 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() host = socket.gethostname()
return socket.gethostbyname(host) return socket.gethostbyname(host)
def get_user_id( # pylint: disable=empty-docstring,unused-argument def get_user_id(self, uow, session): # pylint: disable=unused-argument
self, uow, session """
): 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 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 = {} kwargs = {}
remote_addr = self.get_remote_addr(uow, session) 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") self.assertEqual(plugin.get_remote_addr(None, self.session), "127.0.0.1")
def test_user_id(self): def test_user_id(self):
model = self.app.model
plugin = self.make_plugin() 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)) 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): def test_transaction_args(self):
plugin = self.make_plugin() plugin = self.make_plugin()
with patch.object(socket, "gethostbyname", return_value="127.0.0.1"): with patch.object(socket, "gethostbyname", return_value="127.0.0.1"):