Aug 27 - Update free-plan gate, MFA is no longer bypassable via the app
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user