DRS Support
GA4GH Data Repository Service (DRS) support in Galaxy — resolving and downloading drs:// URIs as a file source, plus an experimental DRS server exposing Galaxy datasets.
Verification anchor: Galaxy origin/dev at SHA de395f9236c12b008dea7e816bd8b05062b565c3. Every path:line below was read at this SHA.
Scope: primary focus is Galaxy as a DRS consumer/client (drs:// URI → download during data import). Galaxy also ships an experimental DRS server (lib/galaxy/webapps/galaxy/api/drs.py), covered briefly as an adjacent facet. Out of scope: GA4GH WES (PR 21335 - GA4GH WES API) and TRS (Component - Tool Shed Search and Indexing), which share only the build_service_info plumbing.
Recent activity (both merged at this SHA):
- PR #20410 “Add DRS compact identifier support” (merge
e384c6f, 2026-01-28) — compact-identifier resolution + S3 access methods, all inutil.py. - PR #22484 “Allow attaching OIDC access token to DRS requests” (merge
f63ada6, 2026-05-21, from AustralianBioCommons) — OIDC bearer-token attachment with expiry tracking.
Table of Contents
- Architecture & Data Flow
- The DRS File-Source Plugin
- The DRS Client
- Compact-Identifier Resolution (PR #20410)
- OIDC Access-Token Attachment (PR #22484)
- Config Surface
- Tests
- Adjacent — Galaxy as a DRS Server
- Known Issues & Limitations
Architecture & Data Flow
DRS is implemented as a Galaxy file source, not a bespoke subsystem. A drs:// URI is dispatched to a file source by URL-scoring, then the DRS plugin delegates to a standalone DRS client that resolves the URI to a concrete download URL and streams it through stream_url_to_file (which re-enters the file-source machinery for the resolved http(s)/s3 URL). See Component - Data Fetch for the surrounding import pipeline.
drs://... URI
→ ConfiguredFileSources.get_file_source_path(uri) lib/galaxy/files/__init__.py:199
→ find_best_match: max score_url_match() > 0 lib/galaxy/files/__init__.py:189
→ DRSFilesSource._realize_to(source_path, native_path) lib/galaxy/files/sources/drs.py:55
→ fetch_drs_to_file(...) lib/galaxy/files/sources/util.py:319
→ [compact id] resolve_compact_identifier_to_url via identifiers.org util.py:284
[or legacy] https://<netloc>/ga4gh/drs/v1/objects/<object_id> util.py:353
→ retry_and_get(get_url) (handles 202 + Retry-After) util.py:72
→ for each access_method: _get_access_info() → (url, headers) util.py:88
→ if s3:// : _download_s3_file() via s3fs util.py:112
→ else : stream_url_to_file() (re-enters file sources) util.py fetch loop
The DRS plugin is read-only (writable=False, drs.py:45; _write_from raises NotImplementedError, drs.py:70).
The DRS File-Source Plugin
lib/galaxy/files/sources/drs.py:
class DRSFilesSource(BaseFilesSource[...])—plugin_type = "drs"(drs.py:33),plugin_kind = PluginKind.drs(drs.py:34; enum memberlib/galaxy/files/sources/__init__.py:62).- Two config classes:
DRSFileSourceTemplateConfiguration(drs.py:19, templatable) andDRSFileSourceConfiguration(drs.py:26, resolved). Fields:url_regex: str = r"^drs://"(drs.py:21,27) — not templated (needed at init before any RuntimeContext exists, commentdrs.py:20). Compiled in__init__(drs.py:49); an assert enforces it is set (drs.py:48).force_http: bool = False(drs.py:22,28) — forceshttpscheme for legacy resolution.http_headers: dict[str,str] = {}(drs.py:23,29) — headers merged into outbound requests; template-expandable in the template config.
__init__applies defaultsid="_drs",label="DRS file",doc="DRS file handler",writable=False(drs.py:41-47).score_url_match(url)(drs.py:75): returns match length (match.span()[1]) ifurl_regexmatches, else0. A longer/more-specificurl_regex(e.g.^drs://mydrssite.org/) out-scores the generic^drs://stock source — this is how a site-specific DRS source is preferred over the stock one._realize_to(drs.py:55): pullsuser_contextfrom the runtime context, copiesconfig.http_headersinto a local dict, and callsfetch_drs_to_file(..., headers=headers or None, force_http=config.force_http)._allowlist(drs.py:52) returnsself._file_sources_config.fetch_url_allowlist.
Dispatch/registration (lib/galaxy/files/__init__.py):
find_best_matchscores every source and returns the highest positive scorer (files/__init__.py:189-197).get_file_source_path(uri)requires"://"in the URI, raisingRequestParameterInvalidExceptionif no handler matches (files/__init__.py:199-208).- Stock plugins: with
load_stock_plugins=True, one stock instance per plugin type is auto-created (id=f"stock_{plugin_type}",files/__init__.py:151-158), i.e. astock_drssource with the default^drs://regex.fetch_drs_to_fileitself falls back toConfiguredFileSources.from_dict(None, load_stock_plugins=True)when no user context is present.
The DRS Client
lib/galaxy/files/sources/util.py (not lib/galaxy/util/) holds all DRS protocol logic.
fetch_drs_to_file(drs_uri, target_path, user_context, force_http=False, retry_options=None, headers=None, fetch_url_allowlist=None)(util.py:319) — the single entry point:- Require
drs://scheme (util.py:330). - If the remainder contains
:, tryresolve_compact_identifier_to_urlfirst (util.py:284); onValueError, fall back to legacy only if a/is present, else re-raise (util.py:340-350). - Legacy fallback: split
netloc/object_id, build{http|https}://{netloc}/ga4gh/drs/v1/objects/{object_id}(httpsunlessforce_http) (util.py:352-360). retry_and_get(get_url, ...)→ parse JSON → require non-emptyaccess_methods(util.py:361-367).- Iterate access methods;
_get_access_infoyields(access_url, access_headers).type == "https"setshttp_headers+fetch_url_allowlist;type == "s3"setsfetch_url_allowlist(util.py:369-390).s3://URLs →_download_s3_filedirectly; elsestream_url_to_file(...). First successbreaks (util.py:392-410). - If nothing downloaded, raise
_not_implemented(...)listing unhandled access-method types (util.py:412-414).
- Require
retry_and_get(get_url, retry_options, headers=None)(util.py:72): GET withDEFAULT_SOCKET_TIMEOUT; on HTTP202honoursRetry-After(required, elseValueError), sleeps, decrementsretry_times(default 5,RetryOptionsutil.py:67), recurses; raises after timeout._get_access_info(obj_url, access_method, headers)(util.py:88): prefersaccess_id(GETs{obj_url}/access/{access_id}for signed/authenticated URLs) over inlineaccess_url; parses"Key: value"header strings into a dict. RaisesValueErrorif neither present._download_s3_file(s3_url, target_path, headers)(util.py:112): signed URLs (containX-Amz-Algorithm/Signature) stream viarequests; raws3://URLs trys3fs.S3FileSystemanonymous → authenticated → requester_pays. Importss3fslazily; raisesImportErrorif absent._not_implemented(drs_uri, desc)(util.py:32): builds aNotImplementedErrorwith remediation text; special-cased guidance for S3 access methods.
Compact-Identifier Resolution (PR #20410)
Added entirely in util.py. A compact identifier is drs://<prefix>:<accession> (e.g. drs://sparc.drs:package/uuid, drs://dg.ANV0:...), resolved via the identifiers.org registry API (n2t.net is not used — only identifiers.org is implemented).
parse_compact_identifier(drs_uri)(util.py:260): stripsdrs://, splits on first:into(prefix, accession); validates prefix chars[a-z0-9._]and both parts non-empty (util.py:271-281).class CompactIdentifierResolver(util.py:171): a process-wide singleton (__new__caches_instance,util.py:175-179) with a_cachedict and default 24h TTL (cache_ttl=86400,util.py:184)._reset_singletonexists for tests (util.py:194)._query_identifiers_org(prefix)(util.py:202): GEThttps://registry.api.identifiers.org/restApi/namespaces/search/findByPrefix?prefix=..., follow_links.resources, pick theofficialresource’surlPattern(else first with aurlPattern) (util.py:203-243). Network/JSON errors are logged and yieldNone.resolve_prefix(prefix)(util.py:245): cache-first, else query + cache.
resolve_compact_identifier_to_url(drs_uri, resolver=None)(util.py:284): resolves prefix →urlPattern, substitutes URL-encoded accession into{$id}, returns the URL. Enforceshttp(s)://result and rejects CR/LF injection (util.py:308-316). Contains provider-specific workarounds for double-prefix duplication: SPARC (sparc.drs+package/...+/package/{$id}) strips thepackage/prefix (util.py:298-303); comment references GTEx-on-AnVIL (dg.ANV0) and ga4gh/data-repository-service-schemas issue #340.
Legacy (host-based) URIs drs://<host>/<object_id> remain supported via the fallback path in fetch_drs_to_file.
OIDC Access-Token Attachment (PR #22484)
Attaches the logged-in user’s OIDC access token as a Bearer Authorization header on outbound DRS/file-source requests, with expiry tracking. Touches auth, files, jobs, exceptions. Related to the header-handling concerns in Component - CORS Handling.
Two attachment mechanisms (both usable from a drs source’s http_headers):
- Template expansion — reference the token directly:
Authorization: "Bearer ${user.oidc_access_tokens['oidc']}"(seetest/unit/files/drs_oidc_file_sources_conf.yml). Backed by the new user-context property. - Declarative provider — set
oidc_auth_provider: <provider-key>on the source; the base class auto-injectsAuthorization: Bearer <token>and records the token’s expiry.
User context (lib/galaxy/files/__init__.py):
- New protocol props
oidc_access_tokens -> dict[str,str] | None(files/__init__.py:362) andoidc_access_token_expirations -> dict[str,datetime](files/__init__.py:365). ProvidesFileSourcesUserContext.oidc_access_tokens(files/__init__.py:437): walksuser.social_auth, collectingextra_data["access_token"]keyed by provider;Nonefor anonymous.oidc_access_token_expirations(files/__init__.py:453) importscompute_token_expiry_for_providerand maps provider→expiry.oidc_access_tokensadded todict_collection_visible_keys(files/__init__.py:327) so it serialises for job-side user contexts;DictFileSourcesUserContextreads both from kwargs (files/__init__.py:515,519).
Files-source properties (lib/galaxy/files/models.py): FilesSourceProperties gains oidc_auth_provider: Optional[str] and auth_expires_at: Optional[str] (ISO-UTC token expiry, set at serialisation time).
Base file source (lib/galaxy/files/sources/__init__.py):
__init__parsesconfig.auth_expires_atintoself._auth_expires_at(sources/__init__.py:367)._check_credentials_fresh()(sources/__init__.py:371): raisesFileSourceCredentialExpiredif now > expiry. Called beforelist,write_from,realize_to(sources/__init__.py:520,578,599)._compute_auth_expires_at(user_context)(sources/__init__.py:377): looks upoidc_access_token_expirations[oidc_auth_provider]._inject_oidc_bearer_token(http_headers, user_context)(sources/__init__.py:386): copies headers andsetdefaultsAuthorization: Bearer <token>— explicitly configured Authorization headers win. ReturnsNoneif no provider/context/token.- Wired into
to_dict(for_serialization=...)(sources/__init__.py:433-438) and_get_runtime_context(sources/__init__.py:479-483).
DRS plugin change: drs.py:_realize_to now copies config.http_headers into a local dict and passes headers=headers or None (drs.py:60,66) — the only functional change PR #22484 made to drs.py (3 lines).
Auth manager / token refresh:
locate_token_expiration(extra_data)moved to module scope (lib/galaxy/authnz/psa_authnz.py:60; old_try_to_locate_refresh_token_expirationdelegates to it,psa_authnz.py:316). Readsexpires/expires_in, or nestedrefresh_tokendict.MinimalJobWrapper._refresh_oidc_tokens_for_job(trans)(lib/galaxy/jobs/__init__.py:1081): callsauthnz_manager.refresh_expiring_oidc_tokens(trans, user)when buildingjob_io(jobs/__init__.py:1092), so a job’s file-source requests use freshly-refreshed tokens.refresh_expiring_oidc_tokensatlib/galaxy/authnz/managers.py:349.
Exception: FileSourceCredentialExpired(MessageException) — status_code=401, err_code FILE_SOURCE_CREDENTIAL_EXPIRED (lib/galaxy/exceptions/__init__.py:177; code 401002 in error_codes.json).
New helper module lib/galaxy/tools/data_fetch_utils.py:
compute_token_expiry_for_provider(user, provider)(data_fetch_utils.py:33):auth_time + expiresas a UTC datetime.iter_fetch_urls(value)(data_fetch_utils.py:11): recursively yieldsurlvalues wheresrc == "url".fetch_uses_authorization_header(request, file_sources, user_context)(data_fetch_utils.py:22): checks whether any fetch URL’s file source serialises anAuthorizationheader.
Flag — apparent dead/forward-looking code: at
de395f9,fetch_uses_authorization_headerhas no caller inlib/(onlycompute_token_expiry_for_provideris imported, atfiles/__init__.py:454+ a unit test).iter_fetch_urlsis used only byfetch_uses_authorization_header. Likely staged for a follow-up. PR #22484’s edit tolib/galaxy/tools/data_fetch.pywas cosmetic (reformatted thedo_fetch(...)call,data_fetch.py:52-56).
Config Surface
lib/galaxy/config/sample/file_sources_conf.yml.sample (lines 238-252) ships two DRS examples:
- A custom source (
type: drs,id: drscustom) withurl_regex: "^drs://mydrssite.org/"and a Cheetah-templated Basic-authhttp_headers.Authorizationfromuser.preferences. Doc warns you should also define a stock source or only this server’s downloads are allowed. - A stock source (
type: drs,id: drsstock) — doc: define this generic DRS source or “stock drs download capability will be disabled” when any other DRS source is configured.
Per-source options: url_regex, force_http, http_headers, plus (PR #22484) oidc_auth_provider / auth_expires_at. fetch_url_allowlist comes from the global files config (consumed via DRSFilesSource._allowlist, drs.py:52).
Server-side GA4GH config (for the DRS server): ga4gh_service_id / ga4gh_service_environment / service-info fields in config_schema.yml:3273,3299,3308-3316 (sample galaxy.yml.sample:2445-2477).
Tests
DRS-consumer tests (see Component - API Tests for the harness):
test/unit/files/test_drs.py— file-source realization + OIDC:test_provides_file_sources_user_context_oidc_access_tokens(+_anonymous),test_drs_http_headers_template_expansion,test_drs_oidc_token_wrong_provider_raises,test_drs_oidc_token_no_tokens_raises,test_file_source_drs_http,test_file_source_drs_s3,test_file_source_drs_attach_oidc_token. Usesresponsesto mock DRS/HTTP. Confs:drs_file_sources_conf.yml,drs_oidc_file_sources_conf.yml.test/unit/files/test_drs_compact_identifiers.py— parse/resolve/cache:test_parse_valid_compact_identifier,test_parse_invalid_prefix_format,test_identifiers_org_resolution,test_cache_behavior,test_full_resolution,test_resolution_with_special_chars,test_invalid_resolved_url,test_fetch_compact_identifier_drs.test/integration/test_drs_compact_identifiers.py—TestDRSCompactIdentifiersIntegration:test_fetch_compact_identifier_drs_file,test_compact_identifier_error_handling,test_meta_resolver_failure_handling.test/unit/app/tools/test_data_fetch_utils.py— importscompute_token_expiry_for_provider.test/unit/webapps/galaxy/services/test_tools_service.py— added by PR #22484 (+78 lines) around fetch/auth-header service behaviour.
DRS-server tests: lib/galaxy_test/api/test_drs.py — class TestDrsApi(ApiTestCase): test_service_info, test_404_on_private_datasets, test_public_data_access, test_public_data_access_util_code, test_exception_handling_schema.
Adjacent — Galaxy as a DRS Server
lib/galaxy/webapps/galaxy/api/drs.py exposes Galaxy datasets over GA4GH DRS v1.2.0 (module docstring: “Implement a DRS server for Galaxy dataset objects (experimental)”):
GET /ga4gh/drs/v1/service-info→build_service_info(... artifact="drs" ...)(shared with WES, fromservices/ga4gh.py).GET|POST /ga4gh/drs/v1/objects/{object_id}→DatasetsService.get_drs_object(...)returning aDrsObject(schemalib/galaxy/schema/drs/).GET|POST /ga4gh/drs/v1/objects/{object_id}/access/{access_id}→ always raisesObjectNotFound(“Access IDs are not implemented for this DRS server”).GET /api/drs_download/{object_id}→ streams the dataset viaFileResponse.
All routes are public=True; private datasets 404. This server is independent of the client path above and shares build_service_info with PR 21335 - GA4GH WES API.
Known Issues & Limitations
- Only
httpsands3DRS access methods are implemented; anything else raises_not_implemented(util.py:412). - Compact-identifier resolution depends on live identifiers.org registry availability; failures are logged and surface as
ValueError. Only identifiers.org (no n2t.net). CompactIdentifierResolveris a process-global singleton with a 24h in-memory cache — no cross-process/persistent cache; stale URL patterns persist up to TTL.- Requester-Pays S3 buckets (e.g. SPARC) are explicitly not supported by Galaxy’s S3 file source.
fetch_uses_authorization_header/iter_fetch_urlsappear unused at this SHA — apparent forward-looking code.- DRS server is explicitly “experimental” and does not implement access IDs.