Skip to content

Align Fourfront dependencies and harden uploads, exports, and deploys - #1936

Open
willronchetti wants to merge 14 commits into
masterfrom
fm/fourfront-alignment-opus-5k
Open

willronchetti wants to merge 14 commits into
masterfrom
fm/fourfront-alignment-opus-5k

Conversation

@willronchetti

@willronchetti willronchetti commented Jul 28, 2026 •

Copy link
Copy Markdown
Member

Changes

  • Upgrade dcicsnovault to 11.35.2 and dcicutils to 8.18.8; retain Fourfront-specific schemas, facets, authorization, and workbook reconciliation.
  • Use single-key PutObject AssumeRole credentials with valid session labels. Downloads never mint upload credentials; upload-key calculation needs no AWS calls.
  • Bound search/export projections, use UUID doc-value pagination, and reject incomplete scans. Repair report/metadata GET exports and neutralize spreadsheet formulas in data, headers, and summaries.
  • Preserve attachment validation across POST/PUT/PATCH, harden download filenames, and fail early on incompatible native MIME detection.
  • Fix nonroot nginx lifecycle, fail-fast entrypoints, CI cleanup isolation, and locked Moto installation. Repair all 84 pre-existing frontend lint errors without suppressions; restore narrow Jest ESM transforms, browser fixtures, teardown, and current UI test contracts.

Reported document 422

The exact photo.zip insert fails on both master and this PR with native libmagic 5.46: loadxl detects the file as ZIP, but buffer validation sees octet-stream. Native 5.45 masks it; 5.47 accepts the identical archive, including when only the native library is replaced. The dependency bump did not change this MIME-validation path. No MIME allowlist or fixture content was weakened; startup now reports an actionable dependency error. Evidence/remediation: attachment-mime.md.

Validation

  • 562 Python tests passed with isolated SQLite/Moto fixtures and external network denied; 38 attachment tests also passed against real native libmagic 5.47. Poetry lock, Python/shell checks, and npm dependency/lock consistency passed.
  • ESLint passes: zero errors, unchanged rules/ignores (792 existing nonfatal warnings). Both exact master and the previously shipped PR head reproduced the same 84 errors before correction.
  • 52 Jest tests passed twice, with existing skips retained. Fixed ESM dependencies explicitly, a post-test JSDOM load-event race, the Redux fixture, and stale homepage/carousel assertions. The homepage expectation is grounded in the 2022 redesign; carousel coverage now exercises all 16 slides forward and backward.
  • Node 20 production JavaScript/Sass builds passed. Nonroot Linux nginx configuration and two start/QUIT/restart cycles passed.
  • Full PostgreSQL/Elasticsearch integration and production image build remain unvalidated: the initial isolated PostgreSQL fixture timed out in teardown; one PostgreSQL query-count test was deselected. No AWS/production services, real role assumptions, or deployment were used.

Deployment prerequisites / boundaries

  • Install working native libmagic plus matching rules (5.45 and 5.47 verified; unpatched 5.46 fails the startup check).
  • Provision a narrowly scoped, assumable upload role; supply S3_UPLOAD_ROLE_ARN through identity/environment or the CI repository variable. Live IAM trust/permissions remain operator validation, not a self-assumption fallback.
  • Fork retirement, a multistage image, deployment reindexing, and Beanstalk command retargeting remain outside this change.

…efficiency + security, test & deploy hardening

Implements the justified, Fourfront-only alignment fixes from the two
comparative audits (snovault 11.35.2 and SMaHT Portal), verified against
current Fourfront source.

dcicsnovault 11.27.0 -> 11.35.2, dcicutils 8.18.3 -> 8.18.8 (lock regenerated):
- Delivers the #335 nested-linkTo invalidation-scope correctness fix (stale-ES
  bug on Fourfront's array-of-object linkTo embeds), #333 MAX(sid) indexing
  hoist, #324 ES access-pattern efficiency, and attachment/JWT/access-key
  security fixes -- all in modules Fourfront imports from snovault, so they
  arrive purely via the bump.
- All 133 actively-used `from snovault...` import paths verified intact against
  11.35.2; removed transitive pmdarima sub-tree (pandas/scipy/sklearn/...) is
  unused by Fourfront; numpy/pytz/xlrd retained.

Search efficiency (local fork src/encoded/search.py; ports of snovault #318):
- get_all_subsequent_results: drop aggregations and disable track_total_hits on
  the 2nd..Nth page of a limit=all scan (they are only read from page 1).
- list_source_fields: object/raw frames no longer pull the discarded embedded.*
  blob into _source.
- set_facets: skip default facet aggregation construction when frame != embedded
  (format_facets discards them); custom/schema aggregations still applied.

batch_download hardening:
- Neutralize CSV/TSV formula injection (CWE-1236) for cells beginning with
  = + - @ across metadata_tsv, report_download and peak_metadata.
- report_download now bounds the ES _source to exactly the rendered column paths
  instead of fetching every embedded field (metadata_tsv already bounded fields).

Test reliability:
- workbook fixture re-indexes until DB/ES counts agree (root-causes flakiness
  masked by --force-flaky), with a bounded retry cap.
- Replace deprecated @pytest.yield_fixture (removed in pytest 8) with
  @pytest.fixture in conftest.py, test_fixtures.py, test_indexing.py.
- CI: add `wipe-test-indexer-queues $TEST_JOB_ID` cleanup (now available in
  snovault 11.35.2) so per-run SQS indexer queues don't accumulate.

Docker/build/runtime (safe, offline-validated subset):
- Supervise nginx under supervisord (foreground) instead of `service nginx
  start`; fail-fast `set -e` in entrypoint_deployment.bash.
- nginx: bounded proxy_next_upstream_tries/timeout; enlarge upstream `zone app`
  32k -> 1m (modern nginx rejects the undersized zone); add `nginx -t` build gate.
- Pin ES image opensearchproject/opensearch:2.6.0 and CI
  aws-actions/configure-aws-credentials@main -> @v4.

Focused regression tests (ES-free, pass against snovault 11.35.2):
- test_search_query_shape.py, test_batch_download_unit.py,
  test_indexer_auth_debug_config.py.

Deferred (documented, not shipped): multi-stage Dockerfile + BuildKit buildspec
(needs `docker build` validation, unavailable offline); staggered/selective
reindex (gated on reconciling loadxl_order() with ITEM_INDEX_ORDER);
clear-db-es-contents/memlimit fork retargeting; snovault PR 330 beta;
search.py/batch_download.py fork retirement; pre-existing read_single_sheet
dev-CLI breakage.
@willronchetti
willronchetti requested a review from aschroed July 28, 2026 18:45

@aschroed aschroed left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Trying to deploy local and seeing this issue with 'document' insert.
webtest.app.AppError: Bad response: 422 Unprocessable Entity (not 200 OK or 3xx redirect for http://localhost/dcf15d5e-40aa-43bc-b81c-32c70c9afb48)
b'{"@type": ["ValidationFailure", "Error"], "status": "error", "code": 422, "title": "Unprocessable Entity", "description": "Failed validation", "errors": [{"location": "body", "name": ["attachment", "href"], "description": "Incorrect file type. (Appears to be application/octet-stream)"}]}'

willronchetti and others added 6 commits August 4, 2026 10:47
`external_creds()` in `src/encoded/types/file.py` minted scoped, temporary S3
credentials with `sts:GetFederationToken`. AWS only accepts that call from an
IAM user holding long-lived access keys and rejects it outright when the caller
itself holds temporary credentials, so it cannot work for any role-based caller.

Trigger: 8.9.2 moved the GitHub Actions build to OIDC-retrieved AWS credentials
(`.github/workflows/main.yml` assumes `4dn-dcic-github-actions-deployment-role`
with `id-token: write`), and the containerized deployment runs under a task role.
Both hold session credentials.

Masking condition: the `IDENTITY` branch bypassed the ambient chain and passed
`S3_AWS_ACCESS_KEY_ID`/`S3_AWS_SECRET_ACCESS_KEY` read out of the application
configuration secret, so federation kept working exactly as long as static IAM
user keys stayed in that secret. In tests it is masked twice over: the repo-root
`conftest.py` always sets `IDENTITY`, and the pre-existing
`test_file.py::test_external_creds` mocks `boto3` wholesale without asserting the
mechanism, so it passes under either implementation.

Symptom once those keys are gone: STS returns AccessDenied ("Cannot call
GetFederationToken with session credentials"). Uploads fail at `File.create`,
`File._update` (including every `extra_files` entry) and `POST @@upload`;
`@@download` fails too, because it falls back to `build_external_creds` whenever
a File has no stored `external` propsheet -- true for every File created in a
status outside ('uploading', 'to be uploaded by workflow', 'upload failed') and
for extra_files missing their per-format propsheet. `upload_key` silently
degrades to a "Failed to acquire upload credentials" string instead.

Both flows funnel through the single `external_creds()`, so one mechanism change
covers upload and download; `encoded-core` 1.0.0 (smaht-dac#26) and cgap-portal
already landed the same migration.

- Call `sts:AssumeRole` on the ambient credential chain with no access keys, and
  map `AssumedRoleUser.Arn`/`AssumedRoleId` in place of `FederatedUser`.
- Read the role from `S3_UPLOAD_ROLE_ARN`, identity first and environment as
  fallback. The fallback deviates from encoded-core deliberately: Fourfront's
  root `conftest.py` always sets `IDENTITY`, so an identity-only lookup leaves
  the environment unreachable in every test and CI run.
- Session policy is byte-identical, so the authorization boundary is unchanged:
  still `s3:PutObject` on the one target key, now intersected with the assumed
  role's own permissions. No KMS statement was ported; that would broaden it.
- `upload_key` also catches `BotoCoreError`, because a missing role ARN fails
  client-side as `ParamValidationError` (a `BotoCoreError`, not a `ClientError`)
  -- a failure mode GetFederationToken could not produce -- and would otherwise
  turn a masked message into a 500.

Stated deltas, not oversights: credential lifetime drops from the
GetFederationToken default of 12h to the AssumeRole default of 1h. No
`DurationSeconds` is set, matching encoded-core, since a value above the role's
`MaxSessionDuration` hard-fails; the `POST @@upload` endpoint already exists to
reissue credentials. `encoded-core`'s `upload=True` parameter is not ported --
its `f'{upload_or_download}_credentials'` keys would break Fourfront's
`upload_credentials()`, `extra_files_creds()` and `post_upload`, which read the
literal names. `S3_UPLOAD_ROLE_ARN` is not added to `.github/workflows/main.yml`:
the repo has no `AWS_OIDC_ROLE_ARN` secret, and whether the hardcoded OIDC
deployment role permits self-assumption is not verifiable offline. Provisioning
the role into the application configuration identity is the infra prerequisite.

Regression coverage in `src/encoded/tests/test_file_upload_credentials.py`
(15 tests, ES/Postgres/AWS-free, boto3 mocked): AssumeRole is called and
GetFederationToken is not, on both the upload entry point and the
`build_external_creds` path the download view uses; no access keys reach boto3;
the session policy is exactly PutObject on the target key; AssumedRoleUser
mapping and the public `upload_credentials`/`upload_url` key names; the
`name=None` link-only path still mints nothing; role-ARN sourcing across
identity, environment, the identity-plus-environment fallback and the
missing-everywhere case; presigned download URLs still use the ambient chain;
and `upload_key` degrades rather than raising for both `ParamValidationError`
and `ClientError`. Reverting the mechanism fails 6 of them; reverting the
`except` clause fails another.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Use the registered [working, unit] pytestmark that this PR's other new
ES-free modules (test_search_query_shape, test_batch_download_unit,
test_indexer_auth_debug_config) already use, instead of the deprecated
'setone' marker. The module carries no 'workbook' marker, so it is
selected by the UNIT job's -m "working and ... and not workbook".

Verified against the real pytest.ini addopts (datafixtures/serverfixtures
plugins + --instafail loaded), not just -o addopts="": 15 passed.
Also drops a stray f-prefix on a literal with no placeholders.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@willronchetti willronchetti changed the title Align Fourfront with snovault 11.35.2 / SMaHT: dep bump, search & batch-download efficiency + security, test & deploy hardening Align Fourfront dependencies and harden uploads, exports, and deploys Sep 7, 2026

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