From 0b6b21c5f8bfe21392738c6cd16312b547c204e8 Mon Sep 17 00:00:00 2001 From: Chris Green Date: Wed, 22 Apr 2026 15:21:25 -0500 Subject: [PATCH] gplazma2-oidc: Add and test support for `storage.poll` WLCG claim Motivation: [JWT Profiles 1.2](https://github.com/WLCG-AuthZ-WG/common-jwt-profile/blob/master/v1.2/profile.md) describes a new claim, `storage.poll` intended for obtaining online/nearline status of files without requiring full read access. We wish to add support for that claim. Modification: - `POLL("storage.poll", true, READ_METADATA)` is added to `WlcgProfileScope`. - Unit tests are added to `WlcgProfileScopeTest.java`. Result: `READ_METADATA` operations are authorized for requestors without read access if the requestor provides a token with the `storage.poll` claim. Target: master Request: 11.2 Patch: https://rb.dcache.org/r/14675/diff/raw/ Closes: Requires-notes: yes Requires-book: no Acked-by: - Tigran Mkrtchyan - Dmitry Litvintsev --- .../oidc/profiles/WlcgProfileScope.java | 8 +++- .../oidc/profiles/WlcgProfileScopeTest.java | 43 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/modules/gplazma2-oidc/src/main/java/org/dcache/gplazma/oidc/profiles/WlcgProfileScope.java b/modules/gplazma2-oidc/src/main/java/org/dcache/gplazma/oidc/profiles/WlcgProfileScope.java index 431a0f68698..2aefebce80c 100644 --- a/modules/gplazma2-oidc/src/main/java/org/dcache/gplazma/oidc/profiles/WlcgProfileScope.java +++ b/modules/gplazma2-oidc/src/main/java/org/dcache/gplazma/oidc/profiles/WlcgProfileScope.java @@ -76,6 +76,12 @@ public enum Operation { */ STAGE("storage.stage", true, LIST, READ_METADATA, DOWNLOAD, Activity.STAGE), + /** + * Query the status of a file, including whether it is online or nearline (tape). This + * scope allows clients to poll for the status of a file without requiring read access. + */ + POLL("storage.poll", true, READ_METADATA), + /** * "Read" or query information about job status and attributes. */ @@ -147,7 +153,7 @@ public WlcgProfileScope(String scope) { checkScopeValid(operation != null, "Unknown operation %s", operationLabel); if (colon == -1) { - checkScopeValid(!operation.isPathRequired(), "Path must be specified"); + checkScopeValid(!operation.isPathRequired(), "Path must be specified for \"%s\"", operationLabel); path = "/"; } else { String scopePath = scope.substring(colon + 1); diff --git a/modules/gplazma2-oidc/src/test/java/org/dcache/gplazma/oidc/profiles/WlcgProfileScopeTest.java b/modules/gplazma2-oidc/src/test/java/org/dcache/gplazma/oidc/profiles/WlcgProfileScopeTest.java index d23ff310f6f..4733b5392bb 100644 --- a/modules/gplazma2-oidc/src/test/java/org/dcache/gplazma/oidc/profiles/WlcgProfileScopeTest.java +++ b/modules/gplazma2-oidc/src/test/java/org/dcache/gplazma/oidc/profiles/WlcgProfileScopeTest.java @@ -60,6 +60,11 @@ public void shouldIdentifyStorageStageScope() { assertTrue(WlcgProfileScope.isWlcgProfileScope("storage.stage:/")); } + @Test + public void shouldIdentifyStoragePollScope() { + assertTrue(WlcgProfileScope.isWlcgProfileScope("storage.poll:/")); + } + @Test public void shouldNotIdentifyStorageWriteScope() { assertFalse(WlcgProfileScope.isWlcgProfileScope("storage.write:/")); @@ -81,6 +86,11 @@ public void shouldRejectStorageStageScopeWithoutPath() { new WlcgProfileScope("storage.stage"); } + @Test(expected = InvalidScopeException.class) + public void shouldRejectStoragePollScopeWithoutPath() { + new WlcgProfileScope("storage.poll"); + } + @Test(expected = InvalidScopeException.class) public void shouldRejectStorageModifyScopeWithoutPath() { new WlcgProfileScope("storage.modify"); @@ -162,6 +172,34 @@ public void shouldParseStageScopeWithNonRootResourcePath() { assertThat(auth.getActivity(), containsInAnyOrder(LIST, READ_METADATA, DOWNLOAD, Activity.STAGE)); } + @Test + public void shouldParsePollScopeWithRootResourcePath() { + WlcgProfileScope scope = new WlcgProfileScope("storage.poll:/"); + + Optional maybeAuth = scope.authorisation(FsPath.create("/VOs/wlcg")); + + assertTrue(maybeAuth.isPresent()); + + Authorisation auth = maybeAuth.get(); + + assertThat(auth.getPath(), equalTo(FsPath.create("/VOs/wlcg"))); + assertThat(auth.getActivity(), containsInAnyOrder(READ_METADATA)); + } + + @Test + public void shouldParsePollScopeWithNonRootResourcePath() { + WlcgProfileScope scope = new WlcgProfileScope("storage.poll:/foo"); + + Optional maybeAuth = scope.authorisation(FsPath.create("/VOs/wlcg")); + + assertTrue(maybeAuth.isPresent()); + + Authorisation auth = maybeAuth.get(); + + assertThat(auth.getPath(), equalTo(FsPath.create("/VOs/wlcg/foo"))); + assertThat(auth.getActivity(), containsInAnyOrder(READ_METADATA)); + } + @Test(expected=InvalidScopeException.class) public void shouldRejectReadScopeWithRelativeResourcePath() { new WlcgProfileScope("storage.read:foo"); @@ -172,6 +210,11 @@ public void shouldRejectStageScopeWithRelativeResourcePath() { new WlcgProfileScope("storage.stage:foo"); } + @Test(expected=InvalidScopeException.class) + public void shouldRejectPollScopeWithRelativeResourcePath() { + new WlcgProfileScope("storage.poll:foo"); + } + @Test public void shouldParseComputeReadScope() { WlcgProfileScope scope = new WlcgProfileScope("compute.read");