Security Boundaries in Spatial QA
A security boundary in geospatial testing is a deterministic, enforceable line that validation code is not permitted to cross: a tenant whose geometry must never join another tenant’s, a coordinate that must never round outside its sensitivity tier, or a memory ceiling a topology check must never breach. As a strategy within Geospatial QA Fundamentals & Architecture, boundary enforcement reframes security as something the test suite asserts on every commit rather than something an auditor inspects after the fact. This matters in spatial pipelines because the failure modes are silent: a botched coordinate reference system (CRS) transform or an over-permissive bounding box does not throw — it produces plausible, wrong, and sometimes confidential output that flows straight into tile servers and analytical warehouses. The patterns here turn isolation, tolerance, and access control into executable predicates that run inside the same gates as your geometry and schema checks.
A Taxonomy of Spatial Security Boundaries
Spatial boundaries fall into four enforceable families, and each maps to a distinct predicate, tolerance strategy, and failure signature. Treating them as one undifferentiated “security” concern is the most common reason boundary checks are too loose to catch real leaks. Before writing any geometric assertion you should pin the spatial tolerance thresholds for the family in play, because a tenant-isolation check and a precision-drift check tolerate error in opposite directions.
| Boundary family | What it enforces | Core predicate | Tolerance strategy | Threshold range | CRS units |
|---|---|---|---|---|---|
| Isolation / tenancy | No cross-tenant or cross-classification spatial join | ST_Disjoint, ST_Intersects |
Hard fail, zero overlap | Exact (no slack) | n/a (relation) |
| Precision / drift | Output geometry stays within sensitivity-tier grid | equals_exact(g, tol) |
Symmetric epsilon | 1e-6–1e-9 deg; 1e-3 m |
degrees / metres |
| Resource / memory | Validation cannot exhaust the runner heap | bounded batch size | Hard cap per batch | 5k–50k features | n/a |
| Access / redaction | Sensitive coordinates never reach a shared sink | snap-to-grid, attribute mask | Coarsen + null | grid ≥ 100 m | metres |
The threshold ranges are deliberately CRS-aware. A 1e-6 epsilon means roughly 0.11 m at the equator in WGS84 (EPSG:4326) but is meaningless in a projected metre-based system such as EPSG:25832, where you instead express tolerance directly in millimetres. Mixing the two is itself a boundary violation — see the gotchas below.
Isolation Boundaries: Tenant and Classification Separation
Isolation boundaries guarantee that geometries from different tenants, security classifications, or licensing tiers are never silently combined. The predicate is relational rather than metric: adjacent administrative zones owned by different tenants must satisfy ST_Disjoint, while a feature claimed by tenant A must ST_Contains only A’s geometries. In Shapely 2.x the vectorised form reads shapely.disjoint(tenant_a, tenant_b), and the assertion fails closed — any returned False is a leak, not a warning. Because spatial joins are the usual leak vector, scope every join through the dataset’s sensitivity tier as defined by your scoping rules for map data validation, so a high-throughput batch can never widen its extent past the tier it was granted.
import shapely
# Isolation assertion: no geometry may straddle two tenant envelopes.
def assert_tenant_isolation(geom, own_envelope, foreign_envelope) -> None:
assert shapely.contains(own_envelope, geom), "geometry escaped its own tenant scope"
assert shapely.disjoint(geom, foreign_envelope), "geometry intersects a foreign tenant"
Precision Boundaries: Tolerance and Coordinate Drift
Precision boundaries cap how far a transformed or round-tripped geometry may move from its source. The risk is twofold: too tight a tolerance floods CI with false positives from ordinary floating-point noise, while too loose a tolerance lets a real CRS or datum error pass as “close enough.” The correct test is a symmetric relative-error bound. For two geometries
In Shapely 2.x use a.equals_exact(b, tolerance=eps) for vertex-level equality within eps, or shapely.hausdorff_distance(a, b) for the envelope above. Load eps from config keyed on CRS units — never hardcode it — and pin it to the source data’s acquisition accuracy.
import shapely
def assert_within_tolerance(source, transformed, eps: float) -> None:
# eps is in the CRS unit of `transformed` (degrees for 4326, metres for projected).
assert shapely.hausdorff_distance(source, transformed) <= eps, "geometry drifted past tolerance"
Resource Boundaries: Memory-Safe Validation
Resource boundaries stop a validation run from becoming a denial-of-service against your own CI. Operations such as ST_Buffer, dense ST_Intersects matrices, and full-table topology validation balloon memory on large polygon sets or high-density point clouds. The boundary is a hard cap on the working set: pre-filter with an R-tree or Quadtree, stream features in bounded batches, and let each batch fall out of scope before the next loads. The pattern pairs naturally with mocking geospatial data for tests, since synthetic fixtures let you exercise the cap deterministically without pulling production-sized dumps onto a shared runner.
import shapely
from shapely import STRtree
def validate_in_batches(geoms, predicate_geom, batch=10_000):
tree = STRtree(geoms) # R-tree pre-filter
candidates = tree.query(predicate_geom) # only nearby features
for start in range(0, len(candidates), batch):
window = candidates[start:start + batch] # bounded working set
yield shapely.intersects(geoms.take(window), predicate_geom)
A Runnable Boundary Suite with pytest
Boundary checks belong in the same harness as the rest of your assertions so they share fixtures, tolerance config, and failure reporting. The suite below loads tolerance from config, exercises all three metric families, and fails fast. It assumes Shapely 2.x and pytest 7+, and it mirrors the unit-versus-integration split covered in when to use unit vs integration tests in GIS — these boundary predicates are pure and in-memory, so they run on every commit.
import shapely
import pytest
# tolerance_config.yaml -> loaded once per session
TOLERANCE = {"epsg_4326_deg": 1e-6, "epsg_25832_m": 1e-3}
@pytest.fixture(scope="session")
def eps():
return TOLERANCE["epsg_4326_deg"]
def test_tenant_isolation(tenant_a, tenant_b):
# Hard boundary: zero tolerance for cross-tenant overlap.
assert shapely.disjoint(tenant_a, tenant_b)
def test_transform_precision(source_geom, reprojected_geom, eps):
# Symmetric boundary: drift must stay under the CRS-scaled epsilon.
assert shapely.hausdorff_distance(source_geom, reprojected_geom) <= eps
def test_redaction_coarsens_below_grid(sensitive_point):
redacted = shapely.set_precision(sensitive_point, grid_size=100.0) # metres
# Boundary: redacted output must not equal the original vertex.
assert not sensitive_point.equals_exact(redacted, tolerance=0.0)
Run the family under a Great Expectations 0.18 custom expectation when the checks must live beside data-quality docs rather than code tests; the predicate is identical, only the wrapper differs.
PostGIS Boundary Counterparts
Server-side enforcement complements the Python suite by catching violations the moment data is written, before any client reads it. The relational predicates translate directly, and a CHECK or trigger turns the boundary into a constraint the database itself refuses to break. Pin libgeos and PROJ versions in the image so the same ST_* semantics run in CI and production.
-- Isolation: reject any insert whose geometry crosses a foreign tenant extent.
ALTER TABLE features ADD CONSTRAINT no_cross_tenant
CHECK (ST_Disjoint(geom, (SELECT extent FROM foreign_tenant_bounds)));
-- Precision: a reprojection view that flags drift past tolerance.
SELECT f.id
FROM features f
JOIN features_4326 s ON s.id = f.id
WHERE ST_HausdorffDistance(ST_Transform(f.geom, 4326), s.geom) > 1e-6; -- degrees
-- Redaction: coarsen sensitive points to a 100 m grid on egress.
SELECT id, ST_SnapToGrid(geom, 100.0) AS geom FROM sensitive_assets;
Pipeline Integration
Boundary predicates only protect production if they sit on a blocking gate. Wire the pytest suite into the pre-merge job so an isolation or precision failure rejects the merge, and run the heavier batched resource checks as a nightly job against staging-scale fixtures. Pin the geometry stack in the container — libgeos, PROJ, and GDAL — because a minor PROJ bump can shift a datum transform by centimetres and quietly trip a tolerance boundary. Emit structured logs (boundary_family, predicate, tolerance, crs, result) so observability tooling can alert on a rising assertion-failure rate before a rollback is needed. This gating shape is shared across the site’s spatial test pattern design and implementation work, and the synthetic inputs it depends on come from test data generation and mocking strategies; the tiered placement of cheap-versus-expensive checks follows the GIS test pyramid, and the CRS gate itself is detailed in automating CRS validation in CI pipelines.
Common Failure Modes and Gotchas
- CRS unit mismatch in the epsilon. A
1e-6tolerance is ~0.11 m inEPSG:4326but a tenth of a millimetre in a metre-based projection — applying a degree epsilon to projected coordinates makes every drift check pass. Always key tolerance on the CRS unit. - DE-9IM boundary-touching slip.
ST_Intersectsreturns true for geometries that merely share an edge, so two tenants touching along a shared border read as overlapping. UseST_Disjointfor isolation andST_Overlaps/ST_Crosseswhen interior overlap is the real concern. - Snap-to-grid sliver artifacts. Redaction via
set_precision/ST_SnapToGridcan collapse a thin polygon into an invalid or empty geometry; assert validity after coarsening rather than assuming the output is still a usable feature. - Anti-meridian-spanning bounding boxes. A naive tenant envelope across ±180° longitude expands to nearly the whole globe, so the isolation check silently permits every join. Split the geometry at the anti-meridian or test in a projected CRS.
- Unbounded buffer before the pre-filter. Calling
ST_Bufferon the full table before applying the R-tree query reintroduces the OOM the resource boundary was meant to prevent — filter first, buffer the survivors. - Tolerance config drifting from container
PROJ. Pinning the epsilon but not thePROJ/GDALversion lets a base-image bump move transforms underneath a fixed threshold; version both together.
Conclusion
Security boundaries become trustworthy only when they are deterministic, versioned, and executed on every commit instead of inspected after deployment. By separating isolation, precision, resource, and redaction boundaries — each with its own predicate and CRS-aware tolerance — a spatial QA suite catches the silent leaks and drift that generic security review misses. For the wider architecture these boundaries plug into, return to Geospatial QA Fundamentals & Architecture.