Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,46 @@ Synthetic data is only schema-valid against the dictionary that generated it, so
refuses a batch that doesn't match the version being uploaded (override with
`--allow-version-mismatch`).

## Verifying download access (check-download)

Registration alone does not prove a file can be downloaded. Two failure modes
are invisible until a user clicks the file in the portal: an Indexd record
with no storage URL (nothing to download, ever), and a record Fence refuses
to sign a URL for. `g3dt indexd check-download` walks the exact chain the
portal hits — Indexd record → storage URL → DRS object → access methods →
Fence signed URL — and reports PASS/FAIL per object, exiting non-zero if any
object fails so it can gate a deployment step.

Run it before a release, and after registering new files. The env selects the
API key secret and the key's JWT selects the commons, so there is no URL to
pass (and none to get wrong).

```bash
g3dt indexd check-download --env staging # sample the 25 newest
g3dt indexd check-download --env staging --limit 50
g3dt indexd check-download --env prod PREFIX/<uuid-1> PREFIX/<uuid-2>
```

With no GUIDs, the newest objects for the env's commons are sampled from the
indexd registry (latest revision per baseid). The registry may live in a
different AWS account than the commons being checked; if the env's AWS
profile cannot reach it, pass GUIDs explicitly.

Reading a failure:

| Symptom | Meaning |
|---|---|
| `Indexd status: 404` | the object is not registered — a registration problem, not a download one |
| `urls: []` / no access methods | registered but with no storage location; it can never download |
| `Access endpoint … 401` | authorization: the API key's user lacks `read-storage` on the record's `authz` resource — an authz gap, not a broken key |
| `Access endpoint … 500` | Fence has the permission but failed to sign — a service-side fault |

On a 401, compare what the record requires
(`https://commons.example.org/index/<did>`, the `authz` field) with what the
key's user actually holds (`https://commons.example.org/user/user`):
downloads require `read-storage` on the record's authz resource, which a user
holding only `create` does not have.

## Development

```bash
Expand Down
69 changes: 63 additions & 6 deletions src/g3dt/cli/indexd_cmds.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
"""`g3dt indexd` — register S3 files with Gen3 indexd (data-plane).
"""`g3dt indexd` — register S3 files with Gen3 indexd, and verify they download.

Long-running, so it supports ``--on ec2``.
``register`` is a long data-plane op, so it supports ``--on ec2``.
``check-download`` is a read-only HTTP check that takes seconds and whose
whole value is the PASS/FAIL in your terminal, so it is local-only.
"""
from __future__ import annotations

from typing import List
from typing import List, Optional

import typer

from g3dt.cli._internal import dispatch
from g3dt.cli._internal import dispatch, runner
from g3dt.cli._internal.dispatch import Target
from g3dt.cli._internal.resolve import study_of
from g3dt.cli._internal.resolve import env_of, study_of

app = typer.Typer(no_args_is_help=True, help="Register files with Gen3 indexd.")
app = typer.Typer(
no_args_is_help=True,
help="Register files with Gen3 indexd and verify download access.",
)

_REGISTER = "services/indexd/register_indexd.py"
_CHECK_DOWNLOAD = "services/indexd/verify_file_access.py"


@app.command()
Expand Down Expand Up @@ -67,3 +73,54 @@ def remote_cli(env_name):
dispatch.run_or_dispatch(
on, env, _REGISTER, build_args, "indexd-register", remote_cli=remote_cli,
)


@app.command(name="check-download")
def check_download(
guids: Optional[List[str]] = typer.Argument(
None,
help="Object GUIDs, e.g. PREFIX/<uuid>. Omit to sample the most "
"recently registered objects from the indexd registry.",
),
env: str = typer.Option(..., "--env", "-e", help="Environment, e.g. test."),
limit: int = typer.Option(
25, "--limit", "-n",
help="How many objects to sample when no GUIDs are given.",
),
key_path: Optional[str] = typer.Option(
None, "--key-path",
help="Break-glass: local Gen3 API key JSON file, instead of the "
"env's secret.",
),
) -> None:
"""Prove registered objects are downloadable end to end.

Walks Indexd -> DRS -> Fence signed URL for each GUID and exits non-zero
if any object fails, so it can gate a deployment step. Read-only and
local-only (seconds, not a long job — there is nothing to dispatch to EC2).

The env selects the API key secret and the key's JWT selects the commons,
so a staging env checks staging. There is no URL to pass.

With no GUIDs, the newest --limit objects for this commons are sampled
from the indexd registry (latest revision per baseid). The registry may
live in a different AWS account than the commons — sampling needs an env
whose AWS profile can reach it; otherwise pass GUIDs explicitly.

Examples:
g3dt indexd check-download --env staging # sample 25 newest
g3dt indexd check-download --env staging --limit 50
g3dt indexd check-download --env prod PREFIX/aaa PREFIX/bbb
"""
# Validate the env before spawning a subprocess: an unknown env should
# fail here with the config error, not deep inside the script.
e = env_of(env)

args: List[str] = ["--env", e.name]
if key_path:
args += ["--key-path", key_path]
if guids:
args += list(guids)
else:
args += ["--limit", str(limit)]
runner.run(runner.python_script(_CHECK_DOWNLOAD, *args))
Loading
Loading