Skip to content

feat(storage): identity-only blob access, drop BLOB_CONNECTION_STRING - #252

Draft
Joaquín Rivero (jQuinRivero) wants to merge 24 commits into
mainfrom
v-joaquinri/feat/identity-only-blob-access
Draft

Joaquín Rivero (jQuinRivero) wants to merge 24 commits into
mainfrom
v-joaquinri/feat/identity-only-blob-access

Conversation

@jQuinRivero

@jQuinRivero Joaquín Rivero (jQuinRivero) commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Description

Lets the apps reach blob storage with their managed identity only, so BLOB_CONNECTION_STRING (an account key) can be removed from deployed apps and shared-key access can later be disabled on the data account.

  • download_blob_to_tempfile / read_blob_range fall back to BLOB_ACCOUNT_URL + identity when no connection string is set.
  • Read links are per-blob, read-only, 60-minute SAS tokens (blob_read_sas), signed with a user-delegation key in Azure and the account key for the emulator. The container-wide stored policy is no longer created.
  • Stored image-layer and label-tile URLs stay unsigned. Fresh tokens are minted at response time for the label project, labeling workspace, project details (previews) and visualizer, including legacy stored URLs that carry an old policy token.
  • functions.bicep and deploy_apps.sh stop setting BLOB_CONNECTION_STRING; deploy_apps.sh deletes the leftover setting. infra/main.json regenerated.
  • Spec and ADR-0008 added.

Out of scope: Azure Files /data mount key, Batch account key, ACS connection string, and the allowSharedKeyAccess: false change (separate, gated follow-up).

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that changes existing behavior)
  • Documentation update
  • Infrastructure / CI change

Checklist

  • I have read CONTRIBUTING.md
  • My changes follow the project's coding standards (PEP 8 for Python, ESLint rules for JS/TS)
  • I have added or updated tests that cover my changes
  • Python tests pass locally (cd hastelib && hatch run test:pytest) and the UI lints clean (cd ui && npm run lint)
  • I have updated the relevant documentation (README, docs/, inline comments)
  • I have added an entry to CHANGELOG.md if this is a user-facing change

Testing

  • python -m pytest hastelib/tests/core/utils/test_blob_signing.py hastelib/tests/core/utils/test_blob_sas.py hastelib/tests/core/utils/test_blob.py -q: passed.
  • Data layer and artifact store tests: passed.
  • python -m pytest hastelib/tests -q --continue-on-collection-errors: 143 failed, 15 errors, 742 passed. The same 158 failures/errors occur on the base commit (local environment: missing osgeo, Windows fcntl); no new failures. hatch run test:pytest and npm run lint were not run.
  • black -l 79 and flake8 clean on touched Python files.
  • Not yet validated on a live environment: the rollout is dev1 with the setting removed, then dev2, prod, then deleting the policy and disabling shared-key access.

Additional context

Rollback for the config change: re-add BLOB_CONNECTION_STRING.

Review follow-up

  • The legacy setting is now deleted only after the app package is published, and the deploy script parses (bash -n).

  • Project-details URLs (previews, labels, training/inference zips) are signed before the ETag is computed.

  • Labeling responses return tileCredentialExpiresAt; the labeling tool, interactive labeler and building validation refresh tile URLs in place before expiry (ported from the community fork's helper, without its partner scoping). Unsaved labels and selections are untouched.

  • The visualizer does the same for the displayed prediction version: GetVisualizerResults returns ileCredentialExpiresAt; the page re-requests that version and swaps only the tile layer URLs (maps are not rebuilt; a version switch drops an in-flight refresh).

  • Tests: node --test src/util/tileCredentialRefresh.test.js passed (9); predictionResults.test.js passed (26); blob and loading-route tests pass; remaining API failures are the Windows-only prediction/footprint route tests. ESLint and the Vite build could not be run locally (no installed UI dependencies).

  • Second review follow-up: footprint and valid-area downloads and nested model artifact zips are re-signed per response; escaped blob names are decoded before signing; query strings are stripped only from HASTE-owned raster URLs; the visualizer refresh updates live layers atomically (failures retry) and applies fresh URLs on version switches.

Record the design and ADR for removing the storage account key from app settings and minting blob-scoped, short-lived read tokens signed by the app identity.
download_blob_to_tempfile and read_blob_range read only BLOB_CONNECTION_STRING and failed when it was empty. Route both through one credential-aware client that prefers the connection string (local emulator) and otherwise uses BLOB_ACCOUNT_URL with the app identity.
Mint read-only, one-blob, expiring tokens with no stored policy, and let the delegation-key cache guarantee a minimum remaining lifetime.
get_file_remote_path signed a container-wide token under a stored policy. Sign a one-blob, one-hour token instead, with the app identity on real Azure and the account key only for the emulator. The container read policy is no longer created, so deleting it revokes old tokens for good.
get_download_url signed a container-wide token under a stored policy. Sign a one-blob, one-hour token instead, using split_blob_url for artifact paths so Azurite hosts resolve. Remove the now-unused container read policy helper.
Blob and queue access is identity-based, so the account-key connection string is no longer set by Bicep or deploy_apps.sh. deploy_apps.sh also deletes the leftover setting from existing apps. Compiled main.json regenerated.
Adds sign_internal_blob_url, sign_titiler_tile_url and sign_label_project_urls so stored URLs can stay unsigned and be given a fresh per-blob read token when handed out.
Label tile URLs and visualizer layers no longer embed a token in persisted data; labels sign only around their own raster read. The label project, labeling workspace and project-details endpoints mint fresh per-blob tokens at response time, including for legacy stored URLs that carried a policy SAS.
@github-actions

Copy link
Copy Markdown

RC artifacts ready

All branch deployment references use the same RC tag:

  • hastegeo_version: 1.0.58rc31
  • training_image_tag: 1.0.58rc31
  • imageprep_image_tag: 1.0.58rc31
  • wheel: hastegeo-1.0.58rc31-py3-none-any.whl

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A deployment-script syntax error blocks deployment, credentials are removed before compatible code is published, and inadequate URL renewal breaks previews, long-running tile sessions, and persisted downloads.

5 open issues
What changed in this PR

This PR moves HASTE’s deployed blob access toward managed identity, replacing container-wide read tokens with short-lived, per-blob SAS tokens. Disabling shared-key access remains a separate rollout step.

Changes:

  • Add identity fallback for blob reads and centralize read-only, 60-minute SAS generation.
  • Refresh preview and tile URLs when serving responses, and remove stored-policy maintenance.
  • Remove BLOB_CONNECTION_STRING from deployment configuration, update tests, and document the design and rollout.
File Description
spec/​features/​identity-only-blob-access/​README.md Defines goals, scope, and success criteria.
spec/​features/​identity-only-blob-access/​design.md Documents signing behavior and rollout.
spec/​architecture/​decisions/​0008-identity-only-blob-access.md Records the identity-based access decision.
infra/​modules/​functions.bicep Removes the blob connection-string setting.
infra/​main.json Updates the generated deployment template.
hastelib/​tests/​core/​utils/​test_blob.py Tests application credential selection.
hastelib/​tests/​core/​utils/​test_blob_signing.py Adds URL-signing and response-helper tests.
hastelib/​tests/​core/​utils/​test_blob_sas.py Tests token scope, expiry, and key refresh.
hastelib/​tests/​core/​utils/​test_blob_access_policy.py Removes obsolete stored-policy tests.
hastelib/​tests/​core/​data_layer/​test_azure_blob_storage_data_layer.py Updates data-layer signing tests.
hastelib/​tests/​core/​artifact_storage/​test_azure_blob_artifact_storage.py Updates artifact download signing tests.
hastelib/​src/​hastegeo/​core/​utils/​blob.py Adds identity fallback and response-time signing helpers.
hastelib/​src/​hastegeo/​core/​utils/​blob_sas.py Centralizes short-lived, per-blob read tokens.
hastelib/​src/​hastegeo/​core/​utils/​blob_access_policy.py Removes stored-policy maintenance.
hastelib/​src/​hastegeo/​core/​processors/​visualizer.py Signs raster sources for visualizer responses.
hastelib/​src/​hastegeo/​core/​processors/​labels.py Stores unsigned tile sources and signs raster reads.
hastelib/​src/​hastegeo/​core/​data_layer/​azure_blob_storage_data_layer.py Replaces container tokens with per-blob signing.
hastelib/​src/​hastegeo/​core/​artifact_storage/​azure_blob_artifact_storage.py Uses per-blob download tokens without stored policies.
api/​hastefuncapi/​function_app.py Signs preview and labeling URLs in responses.
.github/​scripts/​deploy_apps.sh Stops setting and deletes the legacy blob credential.

Review effort: Balanced

Comment thread .github/scripts/deploy_apps.sh Resolved
Comment thread .github/scripts/deploy_apps.sh Resolved
Comment thread api/hastefuncapi/function_app.py Resolved
Comment thread api/hastefuncapi/function_app.py Resolved
The setting was removed before the package was published and a stray edit broke the script's syntax. Delete it after a successful publish so a failed deploy keeps the old code working.
Preview URLs are signed inside the cached load so the ETag covers the tokens the client holds and a 304 cannot revive an expired one. Label and zip download URLs in project details are re-signed the same way.
Label routes now include tileCredentialExpiresAt, matching the lifetime of
the per-blob read tokens signed into the tile URLs, so the UI can refresh
them before they expire.
Tile URLs carry read tokens that expire after 60 minutes. Add a controller
that refreshes them shortly before expiry (and on tab focus), merging only
the imagery URLs into existing state and TileLayers so unsaved labels and
selections are never replaced, with retry and rollback on failure.
Wire the refresh controller into the labeling tool, interactive labeler
(including the swipe pre-event layer) and building validation. Refreshed
imagery URLs are applied to existing TileLayers in place, so drawn labels,
selections and the loaded model stay untouched; a banner offers retry or
reload if a refresh fails.
Lets the page refresh signed tile URLs before they expire.
Re-requests the displayed prediction version and swaps only the tile layer URLs on both maps. The scene key ignores the signed token so maps are not rebuilt, and a version switch drops any refresh in flight.
A refreshed override applies only to the results snapshot it was fetched for, so a newer snapshot with fresher URLs is never shadowed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved URL-signing and refresh defects can leave invalid or expired credentials in place, breaking artifact downloads and imagery access.

5 open issues
2 previously missed issues

In code that hasn't changed since last review

Medium severity Decode escaped blob names before signing

hastelib/​src/​hastegeo/​core/​artifact_storage/​azure_blob_artifact_storage.py:133

When artifact_path contains an escaped blob name such as a%20b.tif, split_blob_url passes that encoded name into the new blob-scoped signature. The actual blob is named a b.tif, so the resulting download token fails authentication. Decode the blob name before signing, as sign_internal_blob_url already does.

Medium severity Preserve query credentials for external raster URLs

hastelib/​src/​hastegeo/​core/​processors/​visualizer.py:40

For a provider or other-account raster URL that requires query credentials, stripping the query here leaves TiTiler with an unauthenticated URL because sign_internal_blob_url cannot re-sign that source. Pass the original URL to the signer, which already replaces internal tokens while preserving external URLs. The same issue occurs in hastelib/src/hastegeo/core/processors/labels.py:80; only strip queries from HASTE-owned blobs when persisting tile templates.

4 resolved since last review

Review effort: Balanced

@github-actions

Copy link
Copy Markdown

RC artifacts ready

All branch deployment references use the same RC tag:

  • hastegeo_version: 1.0.58rc32
  • training_image_tag: 1.0.58rc32
  • imageprep_image_tag: 1.0.58rc32
  • wheel: hastegeo-1.0.58rc32-py3-none-any.whl

@github-actions

Copy link
Copy Markdown

RC artifacts ready

All branch deployment references use the same RC tag:

  • hastegeo_version: 1.0.58rc33
  • training_image_tag: 1.0.58rc33
  • imageprep_image_tag: 1.0.58rc33
  • wheel: hastegeo-1.0.58rc33-py3-none-any.whl

@github-actions

Copy link
Copy Markdown

RC artifacts ready

All branch deployment references use the same RC tag:

  • hastegeo_version: 1.0.58rc34
  • training_image_tag: 1.0.58rc34
  • imageprep_image_tag: 1.0.58rc34
  • wheel: hastegeo-1.0.58rc34-py3-none-any.whl

@github-actions

Copy link
Copy Markdown

RC artifacts ready

All branch deployment references use the same RC tag:

  • hastegeo_version: 1.0.58rc35
  • training_image_tag: 1.0.58rc35
  • imageprep_image_tag: 1.0.58rc35
  • wheel: hastegeo-1.0.58rc35-py3-none-any.whl

…entials

Sign layer footprint and valid-area downloads and the nested model artifact zips at response time, decode escaped blob names before signing artifact downloads, and strip query strings only from HASTE-owned raster URLs so provider credentials survive.
…switch

The refresh now updates the existing layers through one applier that rolls back and throws on failure, so errors reach the controller's retry path and banner. A new results snapshot also updates the imagery layers from its own URLs instead of leaving the old ones in place.
@github-actions

Copy link
Copy Markdown

RC artifacts ready

All branch deployment references use the same RC tag:

  • hastegeo_version: 1.0.58rc36
  • training_image_tag: 1.0.58rc36
  • imageprep_image_tag: 1.0.58rc36
  • wheel: hastegeo-1.0.58rc36-py3-none-any.whl

… check

Azure deploy paths no longer emit it; the code keeps reading it as an emulator fallback.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

URL-signing and refresh-state bugs can strip required imagery credentials, retain expired overlays, and leave stale error banners after version or model changes.

2 open issues
2 previously missed issues

In code that hasn't changed since last review

Medium severity Preserve signed URLs for configured containers

hastelib/​src/​hastegeo/​core/​utils/​blob.py:284

A signed imagery URL in another container of the same account (for example, /provider/image.tif?sig=... when BLOB_CONTAINER=data) loses its credentials here because _internal_blob_client only checks the account endpoint. sign_internal_blob_url then refuses to sign that container, so the visualizer and generated label tiles receive an unsigned source that cannot be read. Check the configured container before stripping the query, matching the signing helper's scope.

Medium severity Clear refresh errors when switching results or models

ui/​src/​Components/​Visualizer/​useVisualizerTileRefresh.js:51

A failed refresh sets failed for the whole hook, but switching versions or loading a different model never clears it. Those operations install fresh credentials and replace the controller, yet this banner remains visible until the new controller's first successful refresh—normally about 55 minutes later. Associate the failure with the results snapshot that failed, as is already done for override, so a successful version/model load does not retain an obsolete error.

4 resolved since last review

Review effort: Balanced

@github-actions

Copy link
Copy Markdown

RC artifacts ready

All branch deployment references use the same RC tag:

  • hastegeo_version: 1.0.58rc37
  • training_image_tag: 1.0.58rc37
  • imageprep_image_tag: 1.0.58rc37
  • wheel: hastegeo-1.0.58rc37-py3-none-any.whl

unsigned_internal_blob_url stripped the query for any blob in the app account, but signing only covers BLOB_CONTAINER, leaving other containers unreadable.
…pshot

A refreshed override with unavailable prediction rasters no longer falls back to stale signed URLs, and a refresh failure no longer outlives the results it failed for.
@github-actions

Copy link
Copy Markdown

RC artifacts ready

All branch deployment references use the same RC tag:

  • hastegeo_version: 1.0.58rc38
  • training_image_tag: 1.0.58rc38
  • imageprep_image_tag: 1.0.58rc38
  • wheel: hastegeo-1.0.58rc38-py3-none-any.whl

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants