This is an automated email from the ASF dual-hosted git repository.
robertlazarski pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/axis-axis2-java-rampart.git
The following commit(s) were added to refs/heads/master by this push:
new eb824bd7 RAMPART-460: allow a custom UsernameToken validator via
RampartConfig
eb824bd7 is described below
commit eb824bd739254d488865f027062ae22e21a1549c
Author: Robert Lazarski <[email protected]>
AuthorDate: Tue Oct 13 02:24:29 2026 -1000
RAMPART-460: allow a custom UsernameToken validator via RampartConfig
Add an optional usernameTokenValidatorClass to RampartConfig, registered in
place of RampartUsernameTokenValidator when set. This is for password stores
that can only accept or reject a username/password pair, such as LDAP, and
so
cannot return the stored password to the callback. Without the setting,
behaviour is unchanged.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../org/apache/rampart/RampartMessageData.java | 36 ++++++++++
.../policy/builders/RampartConfigBuilder.java | 6 ++
.../apache/rampart/policy/model/RampartConfig.java | 26 +++++++
.../resources/org/apache/rampart/errors.properties | 2 +
.../org/apache/rampart/RampartMessageDataTest.java | 48 +++++++++++++
.../policy/builders/RampartConfigBuilderTest.java | 82 ++++++++++++++++++++++
src/site/resources/rampart-config.xsd | 1 +
src/site/xdoc/rampartconfig-guide.xml | 10 +++
8 files changed, 211 insertions(+)
diff --git
a/modules/rampart-core/src/main/java/org/apache/rampart/RampartMessageData.java
b/modules/rampart-core/src/main/java/org/apache/rampart/RampartMessageData.java
index 97e6af78..a649d0d9 100644
---
a/modules/rampart-core/src/main/java/org/apache/rampart/RampartMessageData.java
+++
b/modules/rampart-core/src/main/java/org/apache/rampart/RampartMessageData.java
@@ -61,6 +61,7 @@ import org.apache.wss4j.dom.handler.WSHandlerResult;
import org.apache.wss4j.dom.message.WSSecHeader;
import org.apache.wss4j.dom.message.token.SecurityContextToken;
import org.apache.wss4j.dom.util.WSSecurityUtil;
+import org.apache.wss4j.dom.validate.Validator;
import org.apache.wss4j.dom.engine.WSSecurityEngine;
import org.apache.wss4j.dom.engine.WSSecurityEngineResult;
@@ -231,6 +232,34 @@ public class RampartMessageData {
}
}
+ /**
+ * Resolves the WSS4J Validator for inbound UsernameTokens: the
+ * <code>usernameTokenValidatorClass</code> from the RampartConfig when
set, otherwise
+ * {@link RampartUsernameTokenValidator}. A custom class is for password
stores that can
+ * only accept or reject a username/password pair and so cannot meet the
password
+ * callback contract (RAMPART-460). A configured class that cannot be
loaded fails the
+ * message rather than falling back, so a typo surfaces instead of being
masked.
+ */
+ static Class<?> getUsernameTokenValidatorClass(RampartConfig rampartConfig,
+ ClassLoader classLoader) throws RampartException {
+ String className = rampartConfig == null ? null
+ : rampartConfig.getUsernameTokenValidatorClass();
+ if (className == null || className.isEmpty()) {
+ return RampartUsernameTokenValidator.class;
+ }
+ Class<?> validatorClass;
+ try {
+ validatorClass = Loader.loadClass(classLoader, className);
+ } catch (ClassNotFoundException e) {
+ throw new RampartException("cannotLoadUsernameTokenValidatorClass",
+ new String[]{className}, e);
+ }
+ if (!Validator.class.isAssignableFrom(validatorClass)) {
+ throw new RampartException("notAUsernameTokenValidator", new
String[]{className});
+ }
+ return validatorClass;
+ }
+
public RampartMessageData(MessageContext msgCtx, boolean sender) throws
RampartException {
this.msgContext = msgCtx;
@@ -496,6 +525,13 @@ public class RampartMessageData {
if (axisService != null) {
this.customClassLoader = axisService.getClassLoader();
}
+
+ // Replaces the default registered above only once the policy (and
any
+ // RampartConfigCallbackHandler update) and the service class
loader are known.
+ if (this.policyData != null) {
+ this.config.setValidator(WSConstants.USERNAME_TOKEN,
getUsernameTokenValidatorClass(
+ this.policyData.getRampartConfig(),
this.customClassLoader));
+ }
if(this.sender && this.policyData != null) {
this.secHeader = new WSSecHeader(this.document);
diff --git
a/modules/rampart-core/src/main/java/org/apache/rampart/policy/builders/RampartConfigBuilder.java
b/modules/rampart-core/src/main/java/org/apache/rampart/policy/builders/RampartConfigBuilder.java
index 6b6efdee..2a641ef6 100644
---
a/modules/rampart-core/src/main/java/org/apache/rampart/policy/builders/RampartConfigBuilder.java
+++
b/modules/rampart-core/src/main/java/org/apache/rampart/policy/builders/RampartConfigBuilder.java
@@ -77,6 +77,12 @@ public class RampartConfigBuilder implements
AssertionBuilder<OMElement> {
if (childElement != null) {
rampartConfig.setRampartConfigCbClass(childElement.getText().trim());
}
+
+ childElement = element.getFirstChildWithName(new QName(
+ RampartConfig.NS,
RampartConfig.USERNAME_TOKEN_VALIDATOR_CLASS_LN));
+ if (childElement != null) {
+
rampartConfig.setUsernameTokenValidatorClass(childElement.getText().trim());
+ }
// handle ssl config
childElement = element.getFirstChildWithName(new QName(
diff --git
a/modules/rampart-core/src/main/java/org/apache/rampart/policy/model/RampartConfig.java
b/modules/rampart-core/src/main/java/org/apache/rampart/policy/model/RampartConfig.java
index db97b23d..a9cbcdda 100644
---
a/modules/rampart-core/src/main/java/org/apache/rampart/policy/model/RampartConfig.java
+++
b/modules/rampart-core/src/main/java/org/apache/rampart/policy/model/RampartConfig.java
@@ -35,6 +35,7 @@ import javax.xml.stream.XMLStreamWriter;
* <ramp:encryptionUser>bob</ramp:encryptionUser>
*
<ramp:passwordCallbackClass>org.apache.axis2.security.PWCallback</ramp:passwordCallbackClass>
*
<ramp:policyValidatorCbClass>org.apache.axis2.security.ramp:PolicyValidatorCallbackHandler</ramp:policyValidatorCbClass>
+ *
<ramp:usernameTokenValidatorClass>org.example.MyUsernameTokenValidator</ramp:usernameTokenValidatorClass>
* <ramp:timestampPrecisionInMs>true</timestampPrecisionInMs>
* <ramp:timestampTTL>300</ramp:timestampTTL>
* <ramp:timestampMaxSkew>0</ramp:timestampMaxSkew>
@@ -91,6 +92,8 @@ public class RampartConfig implements Assertion {
public final static String RAMPART_CONFIG_CB_CLASS_LN =
"rampartConfigCallbackClass";
+ public final static String USERNAME_TOKEN_VALIDATOR_CLASS_LN =
"usernameTokenValidatorClass";
+
public final static String SIG_CRYPTO_LN = "signatureCrypto";
public final static String ENCR_CRYPTO_LN = "encryptionCrypto";
@@ -152,6 +155,8 @@ public class RampartConfig implements Assertion {
private String rampartConfigCbClass;
+ private String usernameTokenValidatorClass;
+
private CryptoConfig sigCryptoConfig;
private CryptoConfig encrCryptoConfig;
@@ -292,6 +297,21 @@ public class RampartConfig implements Assertion {
this.policyValidatorCbClass = policyValidatorCbClass;
}
+ public String getUsernameTokenValidatorClass() {
+ return usernameTokenValidatorClass;
+ }
+
+ /**
+ * @param usernameTokenValidatorClass a WSS4J
+ * <code>org.apache.wss4j.dom.validate.Validator</code>
registered for
+ * inbound UsernameTokens in place of
+ * <code>RampartUsernameTokenValidator</code>. It then owns the
whole
+ * password check.
+ */
+ public void setUsernameTokenValidatorClass(String
usernameTokenValidatorClass) {
+ this.usernameTokenValidatorClass = usernameTokenValidatorClass;
+ }
+
public String getRampartConfigCbClass() {
return rampartConfigCbClass;
}
@@ -394,6 +414,12 @@ public class RampartConfig implements Assertion {
writer.writeEndElement();
}
+ if (getUsernameTokenValidatorClass() != null &&
!getUsernameTokenValidatorClass().isEmpty()) {
+ writer.writeStartElement(NS, USERNAME_TOKEN_VALIDATOR_CLASS_LN);
+ writer.writeCharacters(getUsernameTokenValidatorClass());
+ writer.writeEndElement();
+ }
+
writer.writeStartElement(NS, MUST_UNDERSTAND_SECURITY_HEADER_LN);
writer.writeCharacters(Boolean.toString(isMustUnderstandSecurityHeader()));
writer.writeEndElement();
diff --git
a/modules/rampart-core/src/main/resources/org/apache/rampart/errors.properties
b/modules/rampart-core/src/main/resources/org/apache/rampart/errors.properties
index b4df6ed2..f1d39e14 100644
---
a/modules/rampart-core/src/main/resources/org/apache/rampart/errors.properties
+++
b/modules/rampart-core/src/main/resources/org/apache/rampart/errors.properties
@@ -28,6 +28,8 @@ cannotLoadPolicyValidatorCbClass = Cannot load custom policy
validator callback
cannotCreatePolicyValidatorCallbackInstance = Cannot create custom policy
validator callback class instance : \"{0}\"
cannotLoadRampartConfigCallbackClass = Cannot load rampart config callback
class : \"{0}\"
cannotCreateRampartConfigCallbackInstance = Cannot create rampart config
callback class instance : \"{0}\"
+cannotLoadUsernameTokenValidatorClass = Cannot load UsernameToken validator
class : \"{0}\"
+notAUsernameTokenValidator = UsernameToken validator class does not implement
org.apache.wss4j.dom.validate.Validator : \"{0}\"
missingEncryptionUser=Encryption user not specified (The context is created by
the initiating party)
rampartConfigMissing = Rampart policy configuration missing
diff --git
a/modules/rampart-core/src/test/java/org/apache/rampart/RampartMessageDataTest.java
b/modules/rampart-core/src/test/java/org/apache/rampart/RampartMessageDataTest.java
index fcb493c4..f5aa6ffa 100644
---
a/modules/rampart-core/src/test/java/org/apache/rampart/RampartMessageDataTest.java
+++
b/modules/rampart-core/src/test/java/org/apache/rampart/RampartMessageDataTest.java
@@ -25,6 +25,9 @@ import java.util.concurrent.CountDownLatch;
import junit.framework.TestCase;
import org.apache.axis2.description.AxisService;
+import org.apache.rampart.handler.RampartUsernameTokenValidator;
+import org.apache.rampart.policy.model.RampartConfig;
+import org.apache.wss4j.dom.validate.UsernameTokenValidator;
/**
* RAMPART-427: adding the locked CLIENT_SIDE marker parameter to a service
must be
@@ -80,4 +83,49 @@ public class RampartMessageDataTest extends TestCase {
assertNotNull("CLIENT_SIDE parameter should be present",
service.getParameter(RampartMessageData.PARAM_CLIENT_SIDE));
}
+
+ /** A stand-in for a validator that delegates to an external password
store. */
+ public static class ExternalUsernameTokenValidator extends
UsernameTokenValidator {
+ }
+
+ /** RAMPART-460: without the setting the default validator stays in place.
*/
+ public void testDefaultUsernameTokenValidator() throws Exception {
+ assertEquals(RampartUsernameTokenValidator.class,
+ RampartMessageData.getUsernameTokenValidatorClass(null, null));
+ assertEquals(RampartUsernameTokenValidator.class,
+ RampartMessageData.getUsernameTokenValidatorClass(new
RampartConfig(), null));
+ }
+
+ /** RAMPART-460: a configured validator replaces the default. */
+ public void testConfiguredUsernameTokenValidator() throws Exception {
+ RampartConfig config = new RampartConfig();
+
config.setUsernameTokenValidatorClass(ExternalUsernameTokenValidator.class.getName());
+ assertEquals(ExternalUsernameTokenValidator.class,
+ RampartMessageData.getUsernameTokenValidatorClass(config,
null));
+ assertEquals(ExternalUsernameTokenValidator.class,
+ RampartMessageData.getUsernameTokenValidatorClass(config,
+ getClass().getClassLoader()));
+ }
+
+ /** A configured class that is missing must fail, not fall back to the
default. */
+ public void testMissingUsernameTokenValidatorFails() {
+ RampartConfig config = new RampartConfig();
+ config.setUsernameTokenValidatorClass("org.example.NoSuchValidator");
+ try {
+ RampartMessageData.getUsernameTokenValidatorClass(config, null);
+ fail("a missing validator class must be rejected");
+ } catch (RampartException expected) {
+ }
+ }
+
+ /** A configured class that is not a WSS4J Validator must fail up front. */
+ public void testNonValidatorClassFails() {
+ RampartConfig config = new RampartConfig();
+ config.setUsernameTokenValidatorClass(String.class.getName());
+ try {
+ RampartMessageData.getUsernameTokenValidatorClass(config, null);
+ fail("a class that is not a Validator must be rejected");
+ } catch (RampartException expected) {
+ }
+ }
}
diff --git
a/modules/rampart-core/src/test/java/org/apache/rampart/policy/builders/RampartConfigBuilderTest.java
b/modules/rampart-core/src/test/java/org/apache/rampart/policy/builders/RampartConfigBuilderTest.java
new file mode 100644
index 00000000..ff5c0859
--- /dev/null
+++
b/modules/rampart-core/src/test/java/org/apache/rampart/policy/builders/RampartConfigBuilderTest.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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 org.apache.rampart.policy.builders;
+
+import java.io.StringWriter;
+
+import javax.xml.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamWriter;
+
+import junit.framework.TestCase;
+
+import org.apache.axiom.om.OMElement;
+import org.apache.axiom.om.util.AXIOMUtil;
+import org.apache.rampart.policy.model.RampartConfig;
+
+public class RampartConfigBuilderTest extends TestCase {
+
+ private static final String VALIDATOR =
"org.example.LdapUsernameTokenValidator";
+
+ private static RampartConfig build(String body) throws Exception {
+ OMElement element = AXIOMUtil.stringToOM(
+ "<ramp:RampartConfig xmlns:ramp=\"" + RampartConfig.NS + "\">"
+ body
+ + "</ramp:RampartConfig>");
+ return (RampartConfig) new RampartConfigBuilder().build(element, null);
+ }
+
+ /** RAMPART-460: usernameTokenValidatorClass is parsed and written back
out. */
+ public void testUsernameTokenValidatorClassRoundTrip() throws Exception {
+ RampartConfig config = build("<ramp:usernameTokenValidatorClass>\n
" + VALIDATOR
+ + "\n</ramp:usernameTokenValidatorClass>");
+ assertEquals(VALIDATOR, config.getUsernameTokenValidatorClass());
+
+ StringWriter out = new StringWriter();
+ XMLStreamWriter writer =
XMLOutputFactory.newInstance().createXMLStreamWriter(out);
+ config.serialize(writer);
+ writer.flush();
+
+ assertEquals(VALIDATOR,
build(stripRoot(out.toString())).getUsernameTokenValidatorClass());
+ }
+
+ public void testUsernameTokenValidatorClassAbsentByDefault() throws
Exception {
+ assertNull(build("").getUsernameTokenValidatorClass());
+ }
+
+ /** An empty element means the default validator and is not written back
out. */
+ public void testEmptyUsernameTokenValidatorClassNotSerialized() throws
Exception {
+ RampartConfig config = build("<ramp:usernameTokenValidatorClass/>");
+ assertEquals("", config.getUsernameTokenValidatorClass());
+
+ StringWriter out = new StringWriter();
+ XMLStreamWriter writer =
XMLOutputFactory.newInstance().createXMLStreamWriter(out);
+ config.serialize(writer);
+ writer.flush();
+
+
assertFalse(out.toString().contains(RampartConfig.USERNAME_TOKEN_VALIDATOR_CLASS_LN));
+ }
+
+ private static String stripRoot(String xml) throws Exception {
+ OMElement root = AXIOMUtil.stringToOM(xml);
+ StringBuilder sb = new StringBuilder();
+ for (java.util.Iterator<OMElement> it = root.getChildElements();
it.hasNext();) {
+ sb.append(it.next().toString());
+ }
+ return sb.toString();
+ }
+}
diff --git a/src/site/resources/rampart-config.xsd
b/src/site/resources/rampart-config.xsd
index b9e6632b..2b5fc57e 100644
--- a/src/site/resources/rampart-config.xsd
+++ b/src/site/resources/rampart-config.xsd
@@ -16,6 +16,7 @@
<xs:element
name="passwordCallbackClass" type="xs:string" minOccurs="0"/>
<xs:element
name="policyValidatorCbClass" type="xs:string" minOccurs="0"/>
<xs:element
name="rampartConfigCallbackClass" type="xs:string" minOccurs="0"/>
+ <xs:element
name="usernameTokenValidatorClass" type="xs:string" minOccurs="0"/>
<xs:element name="sslConfig"
type="ramp:ssl" minOccurs="0"/>
<xs:element name="kerberosConfig"
type="ramp:ssl" minOccurs="0"/>
<xs:element name="signatureCrypto"
type="ramp:crypto" minOccurs="0"/>
diff --git a/src/site/xdoc/rampartconfig-guide.xml
b/src/site/xdoc/rampartconfig-guide.xml
index 107d8efa..e6bcd93f 100644
--- a/src/site/xdoc/rampartconfig-guide.xml
+++ b/src/site/xdoc/rampartconfig-guide.xml
@@ -28,6 +28,16 @@
<policyValidatorCbClass>
org.apache.axis2.security.CustomPolicyValidater
</policyValidatorCbClass>
+</pre></td></tr>
+ <tr class="a"><td>usernameTokenValidatorClass</td><td>WSS4J Validator
(org.apache.wss4j.dom.validate.Validator)
+ used for inbound UsernameTokens instead of the default
RampartUsernameTokenValidator. Use it when the
+ password store can only accept or reject a username/password pair, for
example LDAP, and cannot return
+ the stored password to the passwordCallbackClass. The class then
performs the whole password check.
+ </td><td>
+<pre>
+<usernameTokenValidatorClass>
+ org.example.LdapUsernameTokenValidator
+</usernameTokenValidatorClass>
</pre></td></tr>
<tr class="a"><td>signatureCrypto</td><td>properties to needed perform
signature, such as crypto
provider, keystore and its password</td><td>