[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