Aug 27 - Update free-plan gate, MFA is no longer bypassable via the app

This commit is contained in:
2026-08-27 12:14:09 -04:00
parent 2d68bad966
commit d291dfc513
8 changed files with 251 additions and 20 deletions
+51 -9
View File
@@ -49,6 +49,17 @@ MIGRATABLE_STATUSES = ('provisioning', 'active', 'suspended')
# Squashed baseline that builds the full schema (migrations/versions root).
BASELINE_REVISION = '0003_add_user_active'
#: The last revision the squashed baseline actually covers.
#:
#: Everything AFTER this point is existence-guarded (INFORMATION_SCHEMA checks,
#: or an idempotent ENUM MODIFY), so it can be replayed over the baseline
#: safely. Everything BEFORE it is not — several pre-phase33 migrations would
#: collide with the full-schema baseline, which is why bootstrap stamps past
#: them instead of running them.
#:
#: If the baseline is ever re-squashed to a later point, move this with it.
BASELINE_COVERS_THROUGH = 'phase32_device_token_columns'
TenantRef = namedtuple('TenantRef', ['id', 'slug', 'db_uri'])
@@ -130,14 +141,34 @@ def upgrade_tenant(tenant, script_location=None, version_locations=None, record_
def bootstrap_tenant(tenant, script_location=None, version_locations=None,
baseline_rev=BASELINE_REVISION, record_job=True):
"""Build a FRESH tenant database, then mark it current.
"""Build a FRESH tenant database, then bring it to head.
Runs the squashed baseline (full schema) only, then `stamp head` — the
historical phase migrations are NOT replayed (several are not idempotent and
would conflict with the full-schema baseline). Use this for brand-new tenant
databases; use upgrade_tenant() for ongoing incremental migrations.
Three steps, and the middle one is the load-bearing part:
Returns the stamped head revision.
1. `upgrade(baseline)` — the squashed baseline builds the bulk of the
schema in one go.
2. `stamp(BASELINE_COVERS_THROUGH)` — declare the DB to be at the last
revision the baseline actually covers, WITHOUT running the pre-phase33
migrations. Those are not idempotent and would collide with the
baseline; skipping them is the whole reason bootstrap exists.
3. `upgrade(head)` — replay the guarded tail (phase33 onward). Every one
of those checks INFORMATION_SCHEMA before touching anything (the two
ENUM widenings use an idempotent MODIFY), so this adds exactly what
the baseline lacks and skips the rest.
This used to stop after step 1 and `stamp('head')` instead — claiming the
database was current when it was missing every column and table added since
phase32. Because the baseline was last refreshed around phase33, a tenant
provisioned that way lacked `users.mfa_enabled` and `users.ui_theme`, and
SQLAlchemy emits every mapped column in its SELECT — so the new workspace
could not even log in. It failed at the first query, not at some optional
feature, and only for freshly provisioned tenants (tenant-zero was adopted
in place with a real schema), which is what kept it hidden.
Use upgrade_tenant() for ongoing incremental migrations of a tenant that
already exists.
Returns the head revision the database ends up at.
"""
db_uri = tenant.db_uri
cfg = _make_config(db_uri, script_location, version_locations)
@@ -153,10 +184,20 @@ def bootstrap_tenant(tenant, script_location=None, version_locations=None,
try:
with contextlib.redirect_stdout(io.StringIO()):
command.upgrade(cfg, baseline_rev) # build full schema (baseline only)
command.stamp(cfg, 'head') # mark at head without replaying phases
command.upgrade(cfg, baseline_rev) # 1. baseline schema
command.stamp(cfg, BASELINE_COVERS_THROUGH) # 2. skip the unguarded past
command.upgrade(cfg, 'head') # 3. replay the guarded tail
applied = current_revision(db_uri)
# A bootstrap that does not end at head has silently produced a broken
# tenant — exactly the failure this sequence exists to prevent. Say so
# here rather than letting it surface as a missing column later.
expected = chain_head(script_location, version_locations)
if applied != expected:
raise RuntimeError(
f'bootstrap ended at {applied!r}, expected head {expected!r}'
f'the tenant database is incomplete')
with control_session() as s:
t = s.get(Tenant, tenant.id)
if t is not None:
@@ -166,7 +207,8 @@ def bootstrap_tenant(tenant, script_location=None, version_locations=None,
if j is not None:
j.status = 'ok'
j.finished_at = now_eastern()
j.log = f'bootstrapped (baseline {baseline_rev}) + stamped {applied}'
j.log = (f'bootstrapped (baseline {baseline_rev}, stamped '
f'{BASELINE_COVERS_THROUGH}, upgraded to {applied})')
return applied
except Exception as e: