mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-23 17:05:28 +08:00
fix(persistence): resolve resource URL migration and scoped SQL failures
- Reflect migration tables without resolving foreign-key targets - Allow validated resource URL expansion through the tenant SQL AST guard - Add regression coverage for trusted and malformed expansion expressions
This commit is contained in:
@@ -35,7 +35,8 @@ def upgrade():
|
||||
if not retained:
|
||||
continue
|
||||
# Reflection intentionally reads literal stored values without expansion.
|
||||
source = sa.Table(table.name, sa.MetaData(), autoload_with=connection)
|
||||
# Only local columns are needed; historical schemas may omit FK targets.
|
||||
source = sa.Table(table.name, sa.MetaData(), autoload_with=connection, resolve_fks=False)
|
||||
retained = [name for name in retained if name in source.c]
|
||||
keys = list(source.primary_key.columns)
|
||||
if len(keys) != 1:
|
||||
|
||||
@@ -412,8 +412,16 @@ def _validate_scoped_statement_call(args: tuple[typing.Any, ...], kwargs: dict[s
|
||||
'TenantUnitOfWork public SQL must use a structured SQLAlchemy query, values, or DML statement'
|
||||
)
|
||||
|
||||
from .resource_urls import ExpandedResourceURLs
|
||||
|
||||
for element in _iter_scoped_statement_elements(statement):
|
||||
if not type(element).__module__.startswith('sqlalchemy.'):
|
||||
resource_expansion_allowed = (
|
||||
type(element) is ExpandedResourceURLs
|
||||
and len(list(element.clauses)) == 1
|
||||
and isinstance(list(element.clauses)[0], sqlalchemy.Column)
|
||||
and type(list(element.clauses)[0].type) is MediaReferenceText
|
||||
)
|
||||
if not type(element).__module__.startswith('sqlalchemy.') and not resource_expansion_allowed:
|
||||
raise ScopedSessionTransactionError(
|
||||
'TenantUnitOfWork does not allow custom SQL AST nodes in public statements'
|
||||
)
|
||||
@@ -503,14 +511,6 @@ def _validate_scoped_statement_call(args: tuple[typing.Any, ...], kwargs: dict[s
|
||||
)
|
||||
|
||||
if isinstance(element, sqlalchemy.sql.functions.FunctionElement):
|
||||
from .resource_urls import ExpandedResourceURLs
|
||||
|
||||
resource_expansion_allowed = (
|
||||
type(element) is ExpandedResourceURLs
|
||||
and len(list(element.clauses)) == 1
|
||||
and isinstance(list(element.clauses)[0], sqlalchemy.Column)
|
||||
and type(list(element.clauses)[0].type) is MediaReferenceText
|
||||
)
|
||||
function_name = str(getattr(element, 'name', '')).casefold()
|
||||
package_names = tuple(getattr(element, 'packagenames', ()))
|
||||
generic_function_allowed = (
|
||||
|
||||
@@ -980,6 +980,27 @@ async def test_scoped_sql_structure_allows_only_the_production_vocabulary(statem
|
||||
_validate_scoped_statement_call((statement,), {})
|
||||
|
||||
|
||||
async def test_resource_expansion_allows_only_the_fixed_function_and_media_column() -> None:
|
||||
from langbot.pkg.entity.persistence.event_log import EventLog
|
||||
from langbot.pkg.persistence.resource_urls import ExpandedResourceURLs
|
||||
|
||||
statement = sa.update(EventLog).where(EventLog.input_json == '{}').values(input_json='[]')
|
||||
_validate_scoped_statement_call((statement,), {})
|
||||
|
||||
class SpoofedExpansion(ExpandedResourceURLs):
|
||||
inherit_cache = True
|
||||
|
||||
for expression in (
|
||||
ExpandedResourceURLs(),
|
||||
ExpandedResourceURLs(sa.literal('{}')),
|
||||
ExpandedResourceURLs(User.uuid),
|
||||
ExpandedResourceURLs(EventLog.input_json, sa.literal('{}')),
|
||||
SpoofedExpansion(EventLog.input_json),
|
||||
):
|
||||
with pytest.raises(ScopedSessionTransactionError, match='custom SQL AST'):
|
||||
_validate_scoped_statement_call((sa.select(expression),), {})
|
||||
|
||||
|
||||
async def test_monitoring_json_casts_are_limited_to_journal_metadata() -> None:
|
||||
from langbot.pkg.entity.persistence.agent_run import AgentRun
|
||||
from langbot.pkg.entity.persistence.event_log import EventLog
|
||||
|
||||
Reference in New Issue
Block a user