[java-plugin-shibd] branch main updated: JSHIBD-28 - Implement some form of flow permissioning

Codeberg noreply at shibboleth.net
Wed Sep 16 01:07:11 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/8de7261d7a0892589109480bac3b520091408961

The following commit(s) were added to refs/heads/main by this push:
     new 8de7261  JSHIBD-28 - Implement some form of flow permissioning
8de7261 is described below

commit 8de7261d7a0892589109480bac3b520091408961
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Tue Sep 15 21:06:59 2026 -0400

    JSHIBD-28 - Implement some form of flow permissioning
    
    https://shibboleth.atlassian.net/browse/JSHIBD-28
    
    Also defaulted in storage beans for flows now that access is blocked.
---
 .../idp/flows/sp/abstract/sp-abstract-beans.xml    |  3 +
 .../idp/flows/sp/sealer/sealer-beans.xml           |  2 +-
 .../shibboleth/idp/flows/sp/sealer/sealer-flow.xml |  1 +
 .../flows/sp/session-cache/session-cache-beans.xml |  6 +-
 .../flows/sp/session-cache/session-cache-flow.xml  |  1 +
 .../idp/flows/sp/storage/storage-beans.xml         |  4 +-
 .../idp/flows/sp/storage/storage-flow.xml          |  1 +
 .../shibboleth/idp/module/conf/sp/sp.properties    | 12 ++--
 .../net/shibboleth/sp/conf/agents-system.xml       |  4 ++
 .../net/shibboleth/sp/flows/SealerFlowTest.java    | 23 +++++--
 .../shibboleth/sp/flows/SessionCacheFlowTest.java  | 45 ++++++++----
 .../net/shibboleth/sp/flows/StorageFlowTest.java   | 39 +++++++----
 .../net/shibboleth/idp/module/conf/sp/agents.xml   |  6 +-
 .../src/main/java/net/shibboleth/sp/Agent.java     | 15 +++-
 .../sp/authn/impl/ValidateAgentOperation.java      | 79 ++++++++++++++++++++++
 .../java/net/shibboleth/sp/impl/BasicAgent.java    | 28 +++++++-
 .../sp/profile/impl/DecodeAgentRequest.java        |  4 +-
 17 files changed, 225 insertions(+), 48 deletions(-)

diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/abstract/sp-abstract-beans.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/abstract/sp-abstract-beans.xml
index 4956ae1..1e9fa90 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/abstract/sp-abstract-beans.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/abstract/sp-abstract-beans.xml
@@ -63,6 +63,9 @@
     <bean id="DefaultCleanupHook"
         class="net.shibboleth.idp.authn.impl.ValidateCredentials.UsernamePasswordCleanupHook" />
 
+    <bean id="ValidateAgentOperation"
+        class="net.shibboleth.sp.authn.impl.ValidateAgentOperation" scope="prototype" />
+
     <bean id="DecodeAgentRequest" class="net.shibboleth.sp.profile.impl.DecodeAgentRequest" scope="prototype"
         p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
         p:populateMDC="%{idp.logging.MDC.enabled:true}" />
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/sealer/sealer-beans.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/sealer/sealer-beans.xml
index a3b1094..10955a7 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/sealer/sealer-beans.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/sealer/sealer-beans.xml
@@ -7,7 +7,7 @@
                            http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
     default-init-method="initialize" default-destroy-method="destroy">
 
-    <bean id="shibboleth.sp.profileId" class="java.lang.String" c:_0="http://shibboleth.net/ns/profiles/sp/parse-request-map" />
+    <bean id="shibboleth.sp.profileId" class="java.lang.String" c:_0="http://shibboleth.net/ns/profiles/sp/sealer" />
     <bean id="shibboleth.sp.loggingId" class="java.lang.String" c:_0="%{sp.service.logging.sealer:Sealer}" />
 
     <bean id="DoSealerOperation"
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/sealer/sealer-flow.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/sealer/sealer-flow.xml
index 7758c50..0d6a680 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/sealer/sealer-flow.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/sealer/sealer-flow.xml
@@ -13,6 +13,7 @@
 
     <!-- Resume flow operation after set up by parent. -->
     <action-state id="DoOperation">
+        <evaluate expression="ValidateAgentOperation" />
         <evaluate expression="DoSealerOperation" />
         <evaluate expression="'proceed'" />
         
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-cache/session-cache-beans.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-cache/session-cache-beans.xml
index 3f6c316..754e839 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-cache/session-cache-beans.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-cache/session-cache-beans.xml
@@ -7,12 +7,12 @@
                            http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
     default-init-method="initialize" default-destroy-method="destroy">
 
-    <bean id="shibboleth.sp.profileId" class="java.lang.String" c:_0="http://shibboleth.net/ns/profiles/sp/parse-request-map" />
-    <bean id="shibboleth.sp.loggingId" class="java.lang.String" c:_0="%{sp.service.logging.storage:Storage}" />
+    <bean id="shibboleth.sp.profileId" class="java.lang.String" c:_0="http://shibboleth.net/ns/profiles/sp/session-cache" />
+    <bean id="shibboleth.sp.loggingId" class="java.lang.String" c:_0="%{sp.service.logging.session-cache:SessionCache}" />
 
     <bean id="DoSessionCacheOperation"
         class="net.shibboleth.sp.profile.impl.DoSessionCacheOperation" scope="prototype"
         p:maxStorageTimeout="%{sp.session.maxStorageTimeout:P1D}"
-        p:storageService-ref="#{'%{sp.session.storageService}'.trim()}" />
+        p:storageService-ref="#{'%{sp.session.storageService:shibboleth.StorageService}'.trim()}" />
 
 </beans>
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-cache/session-cache-flow.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-cache/session-cache-flow.xml
index 8d1c7d2..0cafacd 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-cache/session-cache-flow.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-cache/session-cache-flow.xml
@@ -13,6 +13,7 @@
 
     <!-- Resume flow operation after set up by parent. -->
     <action-state id="DoOperation">
+        <evaluate expression="ValidateAgentOperation" />
         <evaluate expression="DoSessionCacheOperation" />
         <evaluate expression="'proceed'" />
         
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/storage/storage-beans.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/storage/storage-beans.xml
index 6d20521..fa3d2a5 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/storage/storage-beans.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/storage/storage-beans.xml
@@ -7,12 +7,12 @@
                            http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
     default-init-method="initialize" default-destroy-method="destroy">
 
-    <bean id="shibboleth.sp.profileId" class="java.lang.String" c:_0="http://shibboleth.net/ns/profiles/sp/parse-request-map" />
+    <bean id="shibboleth.sp.profileId" class="java.lang.String" c:_0="http://shibboleth.net/ns/profiles/sp/storage" />
     <bean id="shibboleth.sp.loggingId" class="java.lang.String" c:_0="%{sp.service.logging.storage:Storage}" />
 
     <bean id="DoStorageOperation"
         class="net.shibboleth.sp.profile.impl.DoStorageOperation" scope="prototype"
         p:httpServletRequestSupplier-ref="shibboleth.HttpServletRequestSupplier"
-        p:storageService-ref="#{'%{sp.storageService}'.trim()}" />
+        p:storageService-ref="#{'%{sp.storageService:shibboleth.StorageService}'.trim()}" />
 
 </beans>
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/storage/storage-flow.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/storage/storage-flow.xml
index fed19fd..8307e2e 100644
--- a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/storage/storage-flow.xml
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/storage/storage-flow.xml
@@ -13,6 +13,7 @@
 
     <!-- Resume flow operation after set up by parent. -->
     <action-state id="DoOperation">
+        <evaluate expression="ValidateAgentOperation" />
         <evaluate expression="DoStorageOperation" />
         <evaluate expression="'proceed'" />
         
diff --git a/sp-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/sp.properties b/sp-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/sp.properties
index 7d8a730..c3bff7b 100644
--- a/sp-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/sp.properties
+++ b/sp-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/sp.properties
@@ -63,12 +63,12 @@ sp.service.credential.checkInterval = PT5M
 #sp.discoveryURLFunction =
 
 # Settings for Hub-mediated session storage
-#sp.session.storageService =
+#sp.session.storageService = shibboleth.StorageService
 #sp.session.maxStorageTimeout = P1D
 
-###############################
-# Agent Authentication Settings
-###############################
+#########################################
+# Agent Authentication and Authz Settings
+#########################################
 
 # Default agent IP address filter
 #sp.agent.authn.allowedAddressRanges = 127.0.0.1/32, ::1/128
@@ -78,6 +78,10 @@ sp.service.credential.checkInterval = PT5M
 #sp.agent.authn.cached = true
 #sp.agent.authn.cacheDuration = PT1H
 
+# Default utility flows to permit/enable, e.g.:
+#   session-cache, sealer
+#sp.agent.authz.utilityOperations =
+
 
 # Properties exist for optional "enterprise" agent secret validation. Most of them
 # are for LDAP, replicating the large range of properties available in the IdP "proper"
diff --git a/sp-conf-impl/src/main/resources/net/shibboleth/sp/conf/agents-system.xml b/sp-conf-impl/src/main/resources/net/shibboleth/sp/conf/agents-system.xml
index a71ce38..56c72ae 100644
--- a/sp-conf-impl/src/main/resources/net/shibboleth/sp/conf/agents-system.xml
+++ b/sp-conf-impl/src/main/resources/net/shibboleth/sp/conf/agents-system.xml
@@ -36,6 +36,9 @@
     <bean id="DefaultAllowedAddressRanges" parent="shibboleth.CommaDelimStringArray">
         <constructor-arg type="java.lang.String" value="%{sp.agent.authn.allowedAddressRanges:127.0.0.1/32,::1/128}" />
     </bean>
+    <bean id="DefaultAllowedOperations" parent="shibboleth.CommaDelimStringArray">
+        <constructor-arg type="java.lang.String" value="%{sp.agent.authz.utilityOperations:}" />
+    </bean>
 
     <bean id="DefaultDiscoveryServiceStrategy" parent="shibboleth.Functions.Constant"
         c:_0="#{environment.containsProperty('sp.discoveryURL') ? '%{sp.discoveryURL:}'.trim() : null}" />
@@ -45,6 +48,7 @@
         p:authenticationMethod="%{sp.agent.authn.method:}"
         p:allowedAddressRanges-ref="DefaultAllowedAddressRanges"
         p:supportsCachedAuthentication="%{sp.agent.authn.cached:true}"
+        p:authorizedUtilityOperations-ref="DefaultAllowedOperations"
         p:supportsPostPreservation="%{sp.postData.preservation:false}"
         p:postLimit="%{sp.postData.limit:1048576}" />
 
diff --git a/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/SealerFlowTest.java b/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/SealerFlowTest.java
index fcc3289..c8add88 100644
--- a/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/SealerFlowTest.java
+++ b/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/SealerFlowTest.java
@@ -57,6 +57,21 @@ public class SealerFlowTest extends AbstractSPFlowTest {
         return dataSealer;
     }
 
+    /**
+     * Test unauthorized.
+     * 
+     * @throws IOException 
+     */
+    @Test
+    public void unauthorized() throws IOException {
+        setBasicAuth("unauthorized.example.org", "foo");
+        setRequest("POST", new DDF("sealer"));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        assertOutputMessageEvent(result, EventIds.ACCESS_DENIED);
+    }
+    
     /**
      * Test wrap operation with no input.
      */
@@ -80,7 +95,7 @@ public class SealerFlowTest extends AbstractSPFlowTest {
     public void testWrapExpired() throws IOException, DataSealerException {
         setDefaultAuth();
 
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("sealer").structure();
         input.addmember(DoSealerOperation.VALUE).string(TEST_VALUE);
         input.addmember(DoSealerOperation.EXP).longinteger(Instant.now().minusSeconds(3600).toEpochMilli() / 1000);
         setRequest("POST", input);
@@ -111,7 +126,7 @@ public class SealerFlowTest extends AbstractSPFlowTest {
     public void testWrapSuccess() throws IOException, DataSealerException {
         setDefaultAuth();
 
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("sealer").structure();
         input.addmember(DoSealerOperation.VALUE).string(TEST_VALUE);
         setRequest("POST", input);
         
@@ -151,7 +166,7 @@ public class SealerFlowTest extends AbstractSPFlowTest {
 
         final String wrapped = getDataSealer().wrap(AGENT_ID + '!' + TEST_VALUE, Instant.now().minusSeconds(3600));
         
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("sealer").structure();
         input.addmember(DoSealerOperation.VALUE).string(wrapped);
         setRequest("GET", input);
         
@@ -173,7 +188,7 @@ public class SealerFlowTest extends AbstractSPFlowTest {
 
         final String wrapped = getDataSealer().wrap(AGENT_ID + '!' + TEST_VALUE, Instant.now().plusSeconds(3600));
         
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("sealer").structure();
         input.addmember(DoSealerOperation.VALUE).string(wrapped);
         setRequest("GET", input);
         
diff --git a/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/SessionCacheFlowTest.java b/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/SessionCacheFlowTest.java
index 52e4e82..403b63b 100644
--- a/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/SessionCacheFlowTest.java
+++ b/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/SessionCacheFlowTest.java
@@ -78,6 +78,21 @@ public class SessionCacheFlowTest extends AbstractSPFlowTest {
         getStorageService().deleteContext(TEST_CONTEXT);
     }
 
+    /**
+     * Test unauthorized.
+     * 
+     * @throws IOException 
+     */
+    @Test
+    public void unauthorized() throws IOException {
+        setBasicAuth("unauthorized.example.org", "foo");
+        setRequest("POST", new DDF("session-cache"));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        assertOutputMessageEvent(result, EventIds.ACCESS_DENIED);
+    }
+
     /**
      * Test no operation.
      * 
@@ -86,7 +101,7 @@ public class SessionCacheFlowTest extends AbstractSPFlowTest {
     @Test
     public void noOperation() throws IOException {
         setDefaultAuth();
-        setRequest("POST", new DDF(null));
+        setRequest("POST", new DDF("session-cache"));
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         assertFlowExecutionResult(result, FLOW_ID);
         assertFlowExecutionOutcome(result.getOutcome());
@@ -102,7 +117,7 @@ public class SessionCacheFlowTest extends AbstractSPFlowTest {
     public void testReadMissingKey() throws IOException {
         setDefaultAuth();
 
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("session-cache").structure();
         input.addmember(DoSessionCacheOperation.OP).string("R");
         setRequest("POST", input);
 
@@ -121,7 +136,7 @@ public class SessionCacheFlowTest extends AbstractSPFlowTest {
     public void testReadMissingSession() throws IOException {
         setDefaultAuth();
 
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("session-cache").structure();
         input.addmember(DoSessionCacheOperation.OP).string("R");
         input.addmember(DoSessionCacheOperation.KEY).string("foo");
         setRequest("POST", input);
@@ -148,7 +163,7 @@ public class SessionCacheFlowTest extends AbstractSPFlowTest {
         
         getStorageService().create(TEST_CONTEXT, "foo", Base64Support.encode("foo".getBytes(StandardCharsets.UTF_8), false), exp);
         
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("session-cache").structure();
         input.addmember(DoSessionCacheOperation.OP).string("R");
         input.addmember(DoSessionCacheOperation.KEY).string("foo");
         setRequest("POST", input);
@@ -173,7 +188,7 @@ public class SessionCacheFlowTest extends AbstractSPFlowTest {
         
         getStorageService().create(TEST_CONTEXT, "foo", getEncodedSession(), exp);
         
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("session-cache").structure();
         input.addmember(DoSessionCacheOperation.OP).string("R");
         input.addmember(DoSessionCacheOperation.KEY).string("foo");
         setRequest("POST", input);
@@ -202,7 +217,7 @@ public class SessionCacheFlowTest extends AbstractSPFlowTest {
         
         getStorageService().create(TEST_CONTEXT, "foo", getEncodedSession(), exp);
         
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("session-cache").structure();
         input.addmember(DoSessionCacheOperation.OP).string("R");
         input.addmember(DoSessionCacheOperation.KEY).string("foo");
         input.addmember(DoSessionCacheOperation.STORAGE_TIMEOUT).longinteger(900);
@@ -226,7 +241,7 @@ public class SessionCacheFlowTest extends AbstractSPFlowTest {
     public void testDeleteMissing() throws IOException {
         setDefaultAuth();
         
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("session-cache").structure();
         input.addmember(DoSessionCacheOperation.OP).string("D");
         input.addmember(DoSessionCacheOperation.KEY).string("foo");
         setRequest("POST", input);
@@ -251,7 +266,7 @@ public class SessionCacheFlowTest extends AbstractSPFlowTest {
 
         getStorageService().create(TEST_CONTEXT, "foo", getEncodedSession(), exp);
         
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("session-cache").structure();
         input.addmember(DoSessionCacheOperation.OP).string("D");
         input.addmember(DoSessionCacheOperation.KEY).string("foo");
         setRequest("POST", input);
@@ -274,7 +289,7 @@ public class SessionCacheFlowTest extends AbstractSPFlowTest {
     public void testCreateMissingInput() throws IOException, DecodingException {
         setDefaultAuth();
         
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("session-cache").structure();
         input.addmember(DoSessionCacheOperation.OP).string("C");
         
         setRequest("POST", input);
@@ -295,7 +310,7 @@ public class SessionCacheFlowTest extends AbstractSPFlowTest {
     public void testCreateSuccess() throws IOException, DecodingException {
         setDefaultAuth();
         
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("session-cache").structure();
         input.addmember(DoSessionCacheOperation.OP).string("C");
         input.addmember(DoSessionCacheOperation.SESSION).structure().addmember("foo").integer(42);
         
@@ -334,7 +349,7 @@ public class SessionCacheFlowTest extends AbstractSPFlowTest {
         
         getStorageService().create(TEST_CONTEXT, "foo", getEncodedSession(), exp.toEpochMilli());
                 
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("session-cache").structure();
         input.addmember(DoSessionCacheOperation.OP).string("T");
         setRequest("POST", input);
         
@@ -358,7 +373,7 @@ public class SessionCacheFlowTest extends AbstractSPFlowTest {
         
         getStorageService().create(TEST_CONTEXT, "foo", getEncodedSession(), exp.toEpochMilli());
                 
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("session-cache").structure();
         input.addmember(DoSessionCacheOperation.OP).string("T");
         input.addmember(DoSessionCacheOperation.KEY).string("foo");
         input.addmember(DoSessionCacheOperation.STORAGE_TIMEOUT).longinteger(900);
@@ -391,7 +406,7 @@ public class SessionCacheFlowTest extends AbstractSPFlowTest {
 
         getStorageService().create(TEST_CONTEXT, "foo", getEncodedSession(), exp);
                 
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("session-cache").structure();
         input.addmember(DoSessionCacheOperation.OP).string("U");
         input.addmember(DoSessionCacheOperation.KEY).string("foo");
         input.addmember(DoSessionCacheOperation.VERSION).integer(1);
@@ -425,7 +440,7 @@ public class SessionCacheFlowTest extends AbstractSPFlowTest {
 
         getStorageService().create(TEST_CONTEXT, "foo", getEncodedSession(), exp);
 
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("session-cache").structure();
         input.addmember(DoSessionCacheOperation.OP).string("U");
         input.addmember(DoSessionCacheOperation.KEY).string("foo");
         input.addmember(DoSessionCacheOperation.VERSION).integer(2);
@@ -448,7 +463,7 @@ public class SessionCacheFlowTest extends AbstractSPFlowTest {
     public void testUpdateMissing() throws IOException {
         setDefaultAuth();
         
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("session-cache").structure();
         input.addmember(DoSessionCacheOperation.OP).string("U");
         input.addmember(DoSessionCacheOperation.KEY).string("foo");
         input.addmember(DoSessionCacheOperation.VERSION).longinteger(1);
diff --git a/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/StorageFlowTest.java b/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/StorageFlowTest.java
index 8cd53fd..2332a2c 100644
--- a/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/StorageFlowTest.java
+++ b/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/StorageFlowTest.java
@@ -82,6 +82,21 @@ public class StorageFlowTest extends AbstractSPFlowTest {
         getStorageService().deleteContext(AGENT_CONTEXT);
     }
 
+    /**
+     * Test unauthorized.
+     * 
+     * @throws IOException 
+     */
+    @Test
+    public void unauthorized() throws IOException {
+        setBasicAuth("unauthorized.example.org", "foo");
+        setRequest("POST", new DDF("storage"));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        assertOutputMessageEvent(result, EventIds.ACCESS_DENIED);
+    }
+    
     /**
      * Test no operation.
      * 
@@ -90,7 +105,7 @@ public class StorageFlowTest extends AbstractSPFlowTest {
     @Test
     public void noOperation() throws IOException {
         setDefaultAuth();
-        setRequest("POST", new DDF(null));
+        setRequest("POST", new DDF("storage"));
         final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
         assertFlowExecutionResult(result, FLOW_ID);
         assertFlowExecutionOutcome(result.getOutcome());
@@ -106,7 +121,7 @@ public class StorageFlowTest extends AbstractSPFlowTest {
     public void testMissingRead() throws IOException {
         setDefaultAuth();
 
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("storage").structure();
         input.addmember(DoStorageOperation.OP).string("R");
         input.addmember(DoStorageOperation.CONTEXT).string(TEST_CONTEXT);
         input.addmember(DoStorageOperation.KEY).string(TEST_KEY);
@@ -131,7 +146,7 @@ public class StorageFlowTest extends AbstractSPFlowTest {
         
         getStorageService().create(AGENT_CONTEXT, TEST_KEY, TEST_VALUE, exp);
         
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("storage").structure();
         input.addmember(DoStorageOperation.OP).string("R");
         input.addmember(DoStorageOperation.CONTEXT).string(TEST_CONTEXT);
         input.addmember(DoStorageOperation.KEY).string(TEST_KEY);
@@ -159,7 +174,7 @@ public class StorageFlowTest extends AbstractSPFlowTest {
         
         getStorageService().create(AGENT_CONTEXT, TEST_KEY, TEST_VALUE, null);
         
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("storage").structure();
         input.addmember(DoStorageOperation.OP).string("D");
         input.addmember(DoStorageOperation.CONTEXT).string(TEST_CONTEXT);
         input.addmember(DoStorageOperation.KEY).string(TEST_KEY + "2");
@@ -185,7 +200,7 @@ public class StorageFlowTest extends AbstractSPFlowTest {
         
         getStorageService().create(AGENT_CONTEXT, TEST_KEY, TEST_VALUE, null);
         
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("storage").structure();
         input.addmember(DoStorageOperation.OP).string("D");
         input.addmember(DoStorageOperation.CONTEXT).string(TEST_CONTEXT);
         input.addmember(DoStorageOperation.KEY).string(TEST_KEY);
@@ -209,7 +224,7 @@ public class StorageFlowTest extends AbstractSPFlowTest {
     public void successCreate() throws IOException {
         setDefaultAuth();
         
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("storage").structure();
         input.addmember(DoStorageOperation.OP).string("C");
         input.addmember(DoStorageOperation.CONTEXT).string(TEST_CONTEXT);
         input.addmember(DoStorageOperation.KEY).string(TEST_KEY);
@@ -239,7 +254,7 @@ public class StorageFlowTest extends AbstractSPFlowTest {
         
         getStorageService().create(AGENT_CONTEXT, TEST_KEY, TEST_VALUE, null);
         
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("storage").structure();
         input.addmember(DoStorageOperation.OP).string("C");
         input.addmember(DoStorageOperation.CONTEXT).string(TEST_CONTEXT);
         input.addmember(DoStorageOperation.KEY).string(TEST_KEY);
@@ -265,7 +280,7 @@ public class StorageFlowTest extends AbstractSPFlowTest {
         
         final Instant now = Instant.now();
         
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("storage").structure();
         input.addmember(DoStorageOperation.OP).string("U");
         input.addmember(DoStorageOperation.CONTEXT).string(TEST_CONTEXT);
         input.addmember(DoStorageOperation.KEY).string(TEST_KEY);
@@ -297,7 +312,7 @@ public class StorageFlowTest extends AbstractSPFlowTest {
         
         getStorageService().create(AGENT_CONTEXT, TEST_KEY, TEST_VALUE, null);
         
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("storage").structure();
         input.addmember(DoStorageOperation.OP).string("U");
         input.addmember(DoStorageOperation.CONTEXT).string(TEST_CONTEXT);
         input.addmember(DoStorageOperation.KEY).string(TEST_KEY);
@@ -327,7 +342,7 @@ public class StorageFlowTest extends AbstractSPFlowTest {
         
         getStorageService().create(AGENT_CONTEXT, TEST_KEY, TEST_VALUE, null);
         
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("storage").structure();
         input.addmember(DoStorageOperation.OP).string("U");
         input.addmember(DoStorageOperation.CONTEXT).string(TEST_CONTEXT);
         input.addmember(DoStorageOperation.KEY).string(TEST_KEY);
@@ -360,7 +375,7 @@ public class StorageFlowTest extends AbstractSPFlowTest {
         
         getStorageService().create(AGENT_CONTEXT, TEST_KEY, TEST_VALUE, null);
 
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("storage").structure();
         input.addmember(DoStorageOperation.OP).string("U");
         input.addmember(DoStorageOperation.CONTEXT).string(TEST_CONTEXT);
         input.addmember(DoStorageOperation.KEY).string(TEST_KEY);
@@ -389,7 +404,7 @@ public class StorageFlowTest extends AbstractSPFlowTest {
     public void missingUpdateWithVersion() throws IOException {
         setDefaultAuth();
         
-        final DDF input = new DDF().structure();
+        final DDF input = new DDF("storage").structure();
         input.addmember(DoStorageOperation.OP).string("U");
         input.addmember(DoStorageOperation.CONTEXT).string(TEST_CONTEXT);
         input.addmember(DoStorageOperation.KEY).string(TEST_KEY);
diff --git a/sp-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/agents.xml b/sp-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/agents.xml
index 2d22c0b..2dc9056 100644
--- a/sp-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/agents.xml
+++ b/sp-conf-impl/src/test/resources/net/shibboleth/idp/module/conf/sp/agents.xml
@@ -24,7 +24,11 @@
     <!-- ============ Agents and their Applications ============ -->
 
     <bean p:id="testsp.example.org" parent="shibboleth.sp.Agent" p:authenticationMethod="basic"
-        p:credentials="#{ {null : 'foo'} }" />
+        p:credentials="#{{null : 'foo'}}"
+        p:authorizedUtilityOperations="#{{ 'storage', 'sealer', 'session-cache' }}" />
+
+    <bean p:id="unauthorized.example.org" parent="shibboleth.sp.Agent" p:authenticationMethod="basic"
+        p:credentials="#{{null : 'foo'}}" />
     
 
     <!-- ============ RelyingParty Overrides ============ -->
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/Agent.java b/sp-server-api/src/main/java/net/shibboleth/sp/Agent.java
index bc18fbd..854c356 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/Agent.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/Agent.java
@@ -27,6 +27,8 @@ import javax.annotation.concurrent.ThreadSafe;
 
 import net.shibboleth.shared.annotation.constraint.NonNegative;
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.NotLive;
+import net.shibboleth.shared.annotation.constraint.Unmodifiable;
 import net.shibboleth.shared.component.IdentifiedComponent;
 import net.shibboleth.shared.net.IPRange;
 
@@ -52,7 +54,7 @@ public interface Agent extends IdentifiedComponent {
      * 
      * @return set of address ranges
      */
-    @Nonnull Set<IPRange> getAllowedAddressRanges();
+    @Nonnull @NotLive @Unmodifiable Set<IPRange> getAllowedAddressRanges();
 
     /**
      * Get the authentication method to use for the agent.
@@ -77,7 +79,7 @@ public interface Agent extends IdentifiedComponent {
      * 
      * @return set of username/shared secret mappings, either of which may be null
      */
-    @Nonnull Map<String,String> getCredentials();
+    @Nonnull @NotLive @Unmodifiable Map<String,String> getCredentials();
     
     /**
      * Returns true iff the supplied address matches one of the allowed ranges.
@@ -95,6 +97,13 @@ public interface Agent extends IdentifiedComponent {
      */
     boolean isSupportsCachedAuthentication();
     
+    /**
+     * Gets the set of "utility" operations authorized for use by this Agent.
+     * 
+     * @return the set of authorized operations to permit
+     */
+    @Nonnull @NotLive @Unmodifiable Set<String> getAuthorizedUtilityOperations();
+    
     /**
      * Returns true iff the agent is permitted to submit form data to preserve
      * during SSO.
@@ -139,6 +148,6 @@ public interface Agent extends IdentifiedComponent {
      * 
      * @return collection of applications
      */
-    @Nonnull Collection<Application> getApplications();
+    @Nonnull @NotLive @Unmodifiable Collection<Application> getApplications();
 
 }
\ No newline at end of file
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/authn/impl/ValidateAgentOperation.java b/sp-server-impl/src/main/java/net/shibboleth/sp/authn/impl/ValidateAgentOperation.java
new file mode 100644
index 0000000..ac787ac
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/authn/impl/ValidateAgentOperation.java
@@ -0,0 +1,79 @@
+/*
+ * 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.authn.impl;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.springframework.webflow.execution.RequestContext;
+
+import net.shibboleth.idp.profile.context.SpringRequestContext;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.Agent;
+import net.shibboleth.sp.profile.AbstractAgentAction;
+
+/**
+ * An action that authorizes the operation being attempted on behalf of a requesting {@link Agent}.
+ * 
+ * <p>This operates for utility flows that are not assumed to be active in general unless configured
+ * to be.</p>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#ACCESS_DENIED}
+ */
+public class ValidateAgentOperation extends AbstractAgentAction {
+
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(ValidateAgentOperation.class);
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        final Agent agent = ensureAgent();
+        
+        final SpringRequestContext springContext = profileRequestContext.getSubcontext(SpringRequestContext.class);
+        final RequestContext requestContext = springContext != null ? springContext.getRequestContext() : null;
+        if (requestContext == null) {
+            log.warn("{} Request denied from agent '{}', no Spring WebFlow RequestContext available", getLogPrefix(),
+                    agent.getId());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
+            return;
+        }
+        
+        final String flowID = requestContext.getActiveFlow().getId();
+        if (flowID == null || !flowID.startsWith("sp/")) {
+            log.warn("{} Request denied from agent '{}', unexpected flow ID: {}", getLogPrefix(),
+                    agent.getId(), flowID);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
+            return;
+        }
+        
+        final String op = flowID.substring(3);
+        
+        if (!agent.getAuthorizedUtilityOperations().contains(op)) {
+            log.warn("{} Request denied from agent '{}' for unauthorized operation: {}", getLogPrefix(), agent.getId(),
+                    op);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.ACCESS_DENIED);
+            return;
+        }
+        
+        log.debug("{} Request permitted from agent '{}' for operation: {}", getLogPrefix(), agent.getId(), op);
+    }
+    
+}
\ No newline at end of file
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicAgent.java b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicAgent.java
index 467efeb..c9319c3 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicAgent.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicAgent.java
@@ -58,7 +58,7 @@ public class BasicAgent extends BasicApplication implements Agent {
 
     /** Authentication method. */
     @Nullable @NotEmpty String authenticationMethod;
-    
+
     /** Whether to support form submission preservation and recovery. */
     private boolean supportsPostPreservation;
     
@@ -74,6 +74,9 @@ public class BasicAgent extends BasicApplication implements Agent {
     /** Application map. */
     @Nonnull private Map<String,Application> applicationMap;
     
+    /** Authorized utility flows. */
+    @Nonnull private Set<String> authorizedOperations;
+    
     /**
      * Constructor.
      * 
@@ -91,6 +94,7 @@ public class BasicAgent extends BasicApplication implements Agent {
         encoding = StandardCharsets.UTF_8;
         credentials = CollectionSupport.emptyMap();
         applicationMap = CollectionSupport.emptyMap();
+        authorizedOperations = CollectionSupport.emptySet();
         
         setAllowInheritance(false);
     }
@@ -200,6 +204,28 @@ public class BasicAgent extends BasicApplication implements Agent {
         supportsCachedAuthentication = flag;
     }
     
+    /** {@inheritDoc} */
+    @Nonnull @NotLive @Unmodifiable public Set<String> getAuthorizedUtilityOperations() {
+        return authorizedOperations;
+    }
+    
+    /**
+     * Sets the set of "utility" operations authorized for use by this Agent.
+     * 
+     * <p>Defaults to empty.</p>
+     * 
+     * @param ops authorized operation names
+     */
+    public void setAuthorizedUtilityOperations(@Nullable final Collection<String> ops) {
+        checkSetterPreconditions();
+        
+        if (ops != null) {
+            authorizedOperations = CollectionSupport.copyToSet(StringSupport.normalizeStringCollection(ops));
+        } else {
+            authorizedOperations = CollectionSupport.emptySet();
+        }
+    }
+    
     /** {@inheritDoc} */
     public boolean isSupportsPostPreservation() {
         return supportsPostPreservation;
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/DecodeAgentRequest.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/DecodeAgentRequest.java
index 098a837..8473575 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/DecodeAgentRequest.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/DecodeAgentRequest.java
@@ -31,7 +31,6 @@ import org.slf4j.MDC;
 
 import net.shibboleth.shared.annotation.constraint.NotEmpty;
 import net.shibboleth.shared.primitive.LoggerFactory;
-import net.shibboleth.sp.Agent;
 import net.shibboleth.sp.context.AgentRequestContext;
 import net.shibboleth.sp.ddf.DDF;
 import net.shibboleth.sp.messaging.RemotedHttpServletRequest;
@@ -126,7 +125,8 @@ public class DecodeAgentRequest extends AbstractAgentAction {
                 }
                 
                 final String txid = msg.getmember(SPConstants.TXID).string();
-                log.debug("{} Decoded message with transaction ID '{}'", getLogPrefix(), txid);
+                log.debug("{} Decoded message with operation '{}', transaction ID '{}'", getLogPrefix(), msg.name(),
+                        txid);
                 
                 if (populateMDC) {
                     MDC.put(TX_ID_MDC_ATTRIBUTE, txid);

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list