[java-plugin-shibd] branch main updated: JSHIBD-25 - Develop necessary CredentialResolvers for SP service
Codeberg
noreply at shibboleth.net
Wed Aug 19 19:25:17 UTC 2026
This is an automated email from the git hooks/post-receive script.
codeberg pushed a commit to branch main
in repository java-plugin-shibd.
View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd/commit/aaa4c66da24fa90dd6e656f23ca0f7ed570bbd5a
The following commit(s) were added to refs/heads/main by this push:
new aaa4c66 JSHIBD-25 - Develop necessary CredentialResolvers for SP service
aaa4c66 is described below
commit aaa4c66da24fa90dd6e656f23ca0f7ed570bbd5a
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Wed Aug 19 15:25:02 2026 -0400
JSHIBD-25 - Develop necessary CredentialResolvers for SP service
https://shibboleth.atlassian.net/browse/JSHIBD-25
Quick implementation of file-backed storage service and tests.
---
.../sp/storage/impl/FilesystemStorageService.java | 279 +++++++++++++++++++++
.../shibboleth/sp/storage/impl/package-info.java | 18 ++
.../storage/impl/FilesystemStorageServiceTest.java | 228 +++++++++++++++++
3 files changed, 525 insertions(+)
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/storage/impl/FilesystemStorageService.java b/sp-server-impl/src/main/java/net/shibboleth/sp/storage/impl/FilesystemStorageService.java
new file mode 100644
index 0000000..f5f7bea
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/storage/impl/FilesystemStorageService.java
@@ -0,0 +1,279 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.storage.impl;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.storage.AbstractStorageService;
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.VersionMismatchException;
+
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.Positive;
+import net.shibboleth.shared.collection.Pair;
+import net.shibboleth.shared.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.StringSupport;
+
+/**
+ *
+ */
+public class FilesystemStorageService extends AbstractStorageService {
+
+ /** Tracks use of a shared file system for capability method. */
+ private boolean clustered;
+
+ /** Whether service is read only. */
+ private boolean readOnly;
+
+ /** Base location of storage tree in filesystem. */
+ @NonnullAfterInit private String storageBase;
+
+ /**
+ * Sets whether the file system is clustered.
+ *
+ * <p>Defaults to false.</p>
+ *
+ * @param flag flag to set
+ */
+ public void setClustered(final boolean flag) {
+ checkSetterPreconditions();
+
+ clustered = flag;
+ }
+
+ /**
+ * Sets whether the service should operate read-only.
+ *
+ * <p>Defaults to false.</p>
+ *
+ * @param flag flag to set
+ */
+ public void setReadOnly(final boolean flag) {
+ checkSetterPreconditions();
+
+ readOnly = flag;
+ }
+
+ /**
+ * Sets the base/root directory under which objects will be created and deleted.
+ *
+ * <p>Must be an absolute path.</p>
+ *
+ * @param base storage root location
+ */
+ public void setStorageBase(@Nonnull @NotEmpty final String base) {
+ checkSetterPreconditions();
+
+ storageBase = Constraint.isNotNull(StringSupport.trimOrNull(base), "Storage base cannot be null or empty.");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ protected void doInitialize() throws ComponentInitializationException {
+ if (storageBase == null) {
+ throw new ComponentInitializationException("Storage base location cannot be null.");
+ }
+
+ try {
+ final Path storagePath = Path.of(storageBase);
+ if (!storagePath.isAbsolute()) {
+ throw new ComponentInitializationException("Storage base location must be absolute.");
+ }
+
+ final File rootFile = storagePath.toFile();
+ if (!(rootFile.isDirectory() && rootFile.canRead())) {
+ throw new ComponentInitializationException("Storage root must be a readable directory.");
+ }
+
+ if (!readOnly && !rootFile.canWrite()) {
+ throw new ComponentInitializationException("Storage root must be a writeable directory.");
+ }
+
+ } catch (final Exception e) {
+ throw new ComponentInitializationException(e);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean create(@Nonnull @NotEmpty final String context, @Nonnull @NotEmpty final String key,
+ @Nonnull @NotEmpty final String value, @Nullable @Positive final Long expiration) throws IOException {
+ if (expiration != null) {
+ throw new UnsupportedOperationException("Expiration is not supported by this StorageService implementation.");
+ }
+
+ if (readOnly) {
+ throw new IOException("StorageService is read-only.");
+ }
+
+ try {
+ final Path fullPath = Path.of(storageBase, context, key).normalize();
+ if (fullPath.toFile().exists()) {
+ return false;
+ }
+
+ final Path parentFolder = fullPath.getParent();
+ Files.createDirectories(parentFolder);
+ Files.writeString(fullPath, value, StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW);
+ return true;
+ } catch (final Exception e) {
+ throw new IOException(e);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nullable public <T> StorageRecord<T> read(@Nonnull @NotEmpty final String context,
+ @Nonnull @NotEmpty final String key) throws IOException {
+ try {
+ final Path fullPath = Path.of(storageBase, context, key).normalize();
+
+ if (!fullPath.toFile().exists()) {
+ return null;
+ }
+
+ final String value = Files.readString(fullPath);
+ if (value != null) {
+ return new StorageRecord<T>(value, null);
+ }
+
+ return null;
+
+ } catch (final Exception e) {
+ throw new IOException(e);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ @Nonnull public <T> Pair<Long, StorageRecord<T>> read(@Nonnull @NotEmpty final String context,
+ @Nonnull @NotEmpty final String key, long version) throws IOException {
+
+ // We don't handle versions, so the only possible version is 1, thus we either signal that, or
+ // return nothing since there will never be such a version.
+
+ if (version == 1) {
+ return new Pair<>(1L, null);
+ }
+
+ return new Pair<>(null, null);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean update(@Nonnull @NotEmpty final String context, @Nonnull @NotEmpty final String key,
+ @Nonnull @NotEmpty final String value, @Nullable @Positive final Long expiration) throws IOException {
+ throw new UnsupportedOperationException("Updates are not supported by this StorageService implementation.");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public Long updateWithVersion(long version, @Nonnull @NotEmpty final String context,
+ @Nonnull @NotEmpty final String key, @Nonnull @NotEmpty final String value,
+ @Nullable @Positive final Long expiration) throws IOException, VersionMismatchException {
+ throw new UnsupportedOperationException("Updates are not supported by this StorageService implementation.");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean updateExpiration(@Nonnull @NotEmpty final String context, @Nonnull @NotEmpty final String key,
+ @Nullable @Positive final Long expiration) throws IOException {
+ throw new UnsupportedOperationException("Expiration is not supported by this StorageService implementation.");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean delete(@Nonnull @NotEmpty final String context, @Nonnull @NotEmpty final String key)
+ throws IOException {
+ if (readOnly) {
+ throw new IOException("StorageService is read-only.");
+ }
+
+ try {
+ final Path fullPath = Path.of(storageBase, context, key).normalize();
+ return Files.deleteIfExists(fullPath);
+
+ } catch (final Exception e) {
+ throw new IOException(e);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean deleteWithVersion(long version, @Nonnull @NotEmpty final String context,
+ @Nonnull @NotEmpty final String key) throws IOException, VersionMismatchException {
+ if (readOnly) {
+ throw new IOException("StorageService is read-only.");
+ }
+
+ if (version != 1) {
+ throw new VersionMismatchException("Versioning is not supported by this StorageService implementation.");
+ }
+
+ try {
+ final Path fullPath = Path.of(storageBase, context, key).normalize();
+ return Files.deleteIfExists(fullPath);
+
+ } catch (final Exception e) {
+ throw new IOException(e);
+ }
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void reap(@Nonnull @NotEmpty final String context) throws IOException {
+ throw new UnsupportedOperationException("Expiration is not supported by this StorageService implementation.");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void updateContextExpiration(@Nonnull @NotEmpty final String context,
+ @Nullable @Positive final Long expiration) throws IOException {
+ throw new UnsupportedOperationException("Expiration is not supported by this StorageService implementation.");
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void deleteContext(@Nonnull @NotEmpty final String context) throws IOException {
+ if (readOnly) {
+ throw new IOException("StorageService is read-only.");
+ }
+
+ // We could do this. Should we? Probably not....
+
+ throw new UnsupportedOperationException("Context deletion is not supported by this StorageService implementation.");
+ }
+
+ /** {@inheritDoc} */
+ public boolean isServerSide() {
+ return true;
+ }
+
+ /** {@inheritDoc} */
+ public boolean isClustered() {
+ return clustered;
+ }
+
+}
\ No newline at end of file
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/storage/impl/package-info.java b/sp-server-impl/src/main/java/net/shibboleth/sp/storage/impl/package-info.java
new file mode 100644
index 0000000..f66d23d
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/storage/impl/package-info.java
@@ -0,0 +1,18 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Novel {@link org.opensaml.storage.StorageService} implementations.
+ */
+package net.shibboleth.sp.storage.impl;
\ No newline at end of file
diff --git a/sp-server-impl/src/test/java/net/shibboleth/sp/storage/impl/FilesystemStorageServiceTest.java b/sp-server-impl/src/test/java/net/shibboleth/sp/storage/impl/FilesystemStorageServiceTest.java
new file mode 100644
index 0000000..ac9bb77
--- /dev/null
+++ b/sp-server-impl/src/test/java/net/shibboleth/sp/storage/impl/FilesystemStorageServiceTest.java
@@ -0,0 +1,228 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.sp.storage.impl;
+
+import java.io.IOException;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.StorageService;
+import org.opensaml.storage.VersionMismatchException;
+import org.testng.Assert;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Test;
+
+import net.shibboleth.shared.component.ComponentInitializationException;
+
+/**
+ * Unit tests for {@link FilesystemStorageService}.
+ */
+ at SuppressWarnings("javadoc")
+public class FilesystemStorageServiceTest {
+
+ private Path testHome;
+
+ @BeforeMethod
+ public void setUp() throws IOException {
+ testHome = Files.createTempDirectory("test-storage-service");
+ }
+
+ private void tearDownWorker() throws IOException {
+ if (testHome != null) {
+ Files.walkFileTree(testHome, new SimpleFileVisitor<Path>() {
+ @Override
+ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
+ throws IOException
+ {
+ Files.delete(file);
+ return FileVisitResult.CONTINUE;
+ }
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException e)
+ throws IOException
+ {
+ if (e == null) {
+ Files.delete(dir);
+ return FileVisitResult.CONTINUE;
+ }
+ // directory iteration failed
+ throw e;
+ }
+ });
+ testHome = null;
+ }
+ }
+
+ @AfterMethod
+ public void tearDown() throws IOException, InterruptedException {
+ try {
+ tearDownWorker();
+ } catch (final DirectoryNotEmptyException ex) {
+ // We hates the Microsoft Defender. (it pins files so directories cannot be deleted)
+ Thread.sleep(10);
+ tearDownWorker();
+ }
+ }
+
+ @Test
+ public void testSimple() throws ComponentInitializationException, IOException {
+ final StorageService storage = getService(false);
+
+ Assert.assertTrue(storage.create("foo", "bar", "value", null));
+
+ final StorageRecord<?> record = storage.read("foo", "bar");
+ assert record != null;
+ Assert.assertEquals(record.getVersion(), 1);
+ Assert.assertEquals(record.getValue(), "value");
+ Assert.assertNull(record.getExpiration());
+
+ Assert.assertTrue(storage.delete("foo", "bar"));
+
+ Assert.assertNull(storage.read("foo", "bar"));
+ }
+
+ @Test
+ public void testNested() throws ComponentInitializationException, IOException {
+ final StorageService storage = getService(false);
+
+ Assert.assertTrue(storage.create("foo/bar", "zork/frobnitz", "BEGIN\nvalue\nEND\n", null));
+
+ final StorageRecord<?> record = storage.read("foo/bar", "zork/frobnitz");
+ assert record != null;
+ Assert.assertEquals(record.getVersion(), 1);
+ Assert.assertEquals(record.getValue(), "BEGIN\nvalue\nEND\n");
+ Assert.assertNull(record.getExpiration());
+
+ Assert.assertTrue(storage.delete("foo/bar", "zork/frobnitz"));
+
+ Assert.assertNull(storage.read("foo/bar", "zork/frobnitz"));
+ }
+
+ @Test
+ public void testOperationViolations() throws ComponentInitializationException, IOException, VersionMismatchException {
+ final StorageService storage = getService(true);
+
+ try {
+ storage.create("foo", "bar", "value", null);
+ Assert.fail("Should raise IOException");
+ } catch (final IOException e) {
+ // expected
+ }
+
+ try {
+ storage.delete("foo", "bar");
+ Assert.fail("Should raise IOException");
+ } catch (final IOException e) {
+ // expected
+ }
+
+ try {
+ storage.deleteContext("foo");
+ Assert.fail("Should raise IOException");
+ } catch (final IOException e) {
+ // expected
+ }
+
+ try {
+ storage.deleteWithVersion(1, "foo", "bar");
+ Assert.fail("Should raise IOException");
+ } catch (final IOException e) {
+ // expected
+ }
+ }
+
+ @Test
+ public void testUnsupportedOperations() throws ComponentInitializationException, IOException, VersionMismatchException {
+ final StorageService storage = getService(false);
+
+ try {
+ storage.create("foo", "bar", "value", Instant.now().plusSeconds(360).toEpochMilli());
+ Assert.fail("Should raise UnsupportedOperationException");
+ } catch (final UnsupportedOperationException e) {
+ // expected
+ }
+
+ try {
+ storage.update("foo", "bar", "value", null);
+ Assert.fail("Should raise UnsupportedOperationException");
+ } catch (final UnsupportedOperationException e) {
+ // expected
+ }
+
+ try {
+ storage.updateWithVersion(2, "foo", "bar", "value", null);
+ Assert.fail("Should raise UnsupportedOperationException");
+ } catch (final UnsupportedOperationException e) {
+ // expected
+ }
+
+ try {
+ storage.updateExpiration("foo", "bar", null);
+ Assert.fail("Should raise UnsupportedOperationException");
+ } catch (final UnsupportedOperationException e) {
+ // expected
+ }
+
+ try {
+ storage.updateContextExpiration("foo", null);
+ Assert.fail("Should raise UnsupportedOperationException");
+ } catch (final UnsupportedOperationException e) {
+ // expected
+ }
+
+ try {
+ storage.deleteWithVersion(2, "foo", "bar");
+ Assert.fail("Should raise VersionMismatchException");
+ } catch (final VersionMismatchException e) {
+ // expected
+ }
+
+ try {
+ storage.deleteContext("foo");
+ Assert.fail("Should raise UnsupportedOperationException");
+ } catch (final UnsupportedOperationException e) {
+ // expected
+ }
+ }
+
+ /**
+ * Instantiates the service with the specified option.
+ *
+ * @param readOnly flag to pass to service
+ *
+ * @return the service
+ *
+ * @throws ComponentInitializationException
+ */
+ @Nonnull private FilesystemStorageService getService(final boolean readOnly)
+ throws ComponentInitializationException {
+
+ final FilesystemStorageService ss = new FilesystemStorageService();
+ ss.setReadOnly(readOnly);
+ ss.setStorageBase(testHome.toString());
+ ss.initialize();
+ return ss;
+ }
+
+}
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.
More information about the commits
mailing list