loom.core.repository.sqlalchemy.rls

Row-level security support for the SQLAlchemy backend.

The product declares its database users, names and schema here; loom installs a static guard and configures it from that declaration as bound data, and never names anything itself.

class loom.core.repository.sqlalchemy.rls.BootstrapConfig(schema, roles, database_users, names, revoke_public=False, scram_iterations=4096)[source]

Bases: object

Everything the bootstrap applies for one application schema; no name has a default.

revoke_public revokes every PUBLIC privilege on the public schema; it is opt-in because that schema belongs to the whole database. scram_iterations is the PBKDF2 iteration count of every password verifier; it defaults to Postgres’ own scram_iterations (4096) and may only be raised.

Parameters:
property bypass_users: tuple[str, ...]

The declared users with access="bypass", in declaration order.

validated()[source]

Return self after checking every name Postgres will see.

Raises:

ValueError – Naming the first identifier Postgres would fold, truncate or resolve to something else, or two roles sharing a name.

Return type:

BootstrapConfig

document()[source]

The bound JSON document the guard’s configure consumes.

Return type:

dict[str, object]

class loom.core.repository.sqlalchemy.rls.Finding(table, check, expected, actual)[source]

Bases: object

One difference between the declaration and the database.

Parameters:
  • table (str | None)

  • check (str)

  • expected (str)

  • actual (str)

class loom.core.repository.sqlalchemy.rls.Report(ok, findings)[source]

Bases: object

The outcome of verify; ok when nothing differs.

Parameters:
class loom.core.repository.sqlalchemy.rls.DatabaseRoles(owner, migrator)[source]

Bases: object

The two database roles every scoped schema needs; unrelated to RBAC roles.

Parameters:
class loom.core.repository.sqlalchemy.rls.DatabaseUser(login, access)[source]

Bases: object

A database user the product declares, with how far it may reach.

Parameters:
  • login (bool)

  • access (Literal['read', 'write', 'bypass'])

class loom.core.repository.sqlalchemy.rls.SQLAlchemyElevationSink[source]

Bases: object

Sets and clears elevation flags on the transaction of the active session.

class loom.core.repository.sqlalchemy.rls.SchemaNames(guard, readers, writers, version_table, data_version_table)[source]

Bases: object

The names around one application schema; declared by the product, never defaulted.

loom schema init proposes them with derived() and writes them into the product’s configuration, where the product may change any of them.

Parameters:
  • guard (str)

  • readers (str)

  • writers (str)

  • version_table (str)

  • data_version_table (str)

classmethod derived(schema)[source]

Propose the conventional names for schema; used only to write the configuration.

Parameters:

schema (str)

Return type:

SchemaNames

async loom.core.repository.sqlalchemy.rls.apply_bootstrap(superuser_url, config, passwords, *, salt_factory=lambda : ..., lock_timeout='5s')[source]

Install or upgrade the guard of config.schema and configure it, in one transaction.

Raises:

ConfigError – When a name or the lock timeout is invalid, the server refuses to create the event triggers (superuser or rds_superuser needed), the guard holds revisions this release does not know, or the guard differs from the released one before commit.

Parameters:
Return type:

None

async loom.core.repository.sqlalchemy.rls.create_schema(migrator_url, application, *, lock_timeout='5s')[source]

Run create_all on the application’s metadata through the migrator.

The schema’s advisory lock is taken first, the guard’s revision is checked before any guard call, and the listeners registered at compile time open the hatch and call the guard, guard first, for every scoped table, so each table is protected in the transaction that creates it; the guard’s assertion closes the transaction.

Raises:

ConfigError – When scoped tables exist on a non-Postgres dialect without the opt-out, when the guard of the application schema is missing, unknown or below the minimum compatible revision, or when the migrator does not land in the application schema.

Parameters:
Return type:

None

async loom.core.repository.sqlalchemy.rls.detach_range_partitions(target, application, model, start, end, *, interval='month', drop=False, lock_timeout='5s')[source]

Detach, and with drop drop, the partitions of model covering [start, end).

Each partition is unprotected and detached by the guard; a partition that no longer exists is skipped. A detached table keeps its rows without row-level security and without group grants; only the owner and the bypass users reach it.

Returns:

The names of the partitions detached, in order.

Raises:
Parameters:
  • target (str | AsyncConnection)

  • application (Application)

  • model (type | str)

  • start (dt.date)

  • end (dt.date)

  • interval (Interval)

  • drop (bool)

  • lock_timeout (str)

Return type:

tuple[str, …]

async loom.core.repository.sqlalchemy.rls.elevate(scope, decision, *, at, catalog=None)[source]

Elevate scope for the current execution after checking decision.

Raises:
  • RuntimeError – Outside an execution frame.

  • ConfigError – Without a product, or when the product did not map scope.

  • PermissionError – When the decision does not justify the elevation.

Parameters:
Return type:

None

async loom.core.repository.sqlalchemy.rls.ensure_range_partitions(target, application, model, start, end, *, interval='month', lock_timeout='5s')[source]

Create the partitions of model covering [start, end) that do not exist yet.

target is the migrator’s URL, run in its own transaction, or an open connection of the migrator, run inside the caller’s transaction. Each partition is created, registered and protected by the guard in that transaction.

Returns:

The names of the partitions created, in order.

Raises:
  • ConfigError – When model is not a range-partitioned scoped table of the application, the connection does not act as the schema’s owner inside it, or the guard predates partition support.

  • ValueError – When interval is unknown or a partition name is too long.

Parameters:
  • target (str | AsyncConnection)

  • application (Application)

  • model (type | str)

  • start (dt.date)

  • end (dt.date)

  • interval (Interval)

  • lock_timeout (str)

Return type:

tuple[str, …]

loom.core.repository.sqlalchemy.rls.install_pool_reset(engine)[source]

Run RESET ALL whenever a connection returns to the pool.

The open transaction is rolled back first and the reset is committed, because Postgres reverts session-level settings changed inside a transaction that is later rolled back, which is exactly what the pool does on return.

Parameters:

engine (sqlalchemy.ext.asyncio.AsyncEngine)

Return type:

None

loom.core.repository.sqlalchemy.rls.register_scope_source(name, source)[source]

Publish request.<name>; registering a name twice is an error.

Parameters:
Return type:

None

loom.core.repository.sqlalchemy.rls.rls_session_settings(application, product=None)[source]

Build the provider for application; the product’s settings are merged in.

Raises:
  • ConfigError – When a scope binding is missing, malformed or unregistered.

  • ValueError – At call time, when the product emits a key under loom.scope..

Parameters:
  • application (Application)

  • product (SessionSettings | None)

Return type:

Callable[[], Mapping[str, str]]

loom.core.repository.sqlalchemy.rls.validate_elevations(product, scoped)[source]

Fail at startup when elevations names a scope that is unknown or not elevable.

Parameters:
Return type:

None

async loom.core.repository.sqlalchemy.rls.verify(url, application)[source]

Compare the database with the application’s declaration.

Parameters:
Return type:

Report