[KEYCLOAK-12998] Prevent duplicate resources from being added to the keycloak-saml subsystem

- Fixes an issue in parser where the closing tag of the IDP element was in the wrong place, which could break the server configuration
 - Parser now checks for duplicates of elements described with maxOccurs=1 in the schema
 - Add handler for SP and IDP now check for existing SPs or IDPs in the config, preventing addition of a duplicate resource via CLI
 - Subsystem test was enhanced so it now tests some invalid configs with duplicate elements
This commit is contained in:
Stefan Guilhen 2020-03-30 23:24:51 -03:00 committed by Hynek Mlnařík
parent 753c21e9ef
commit 76717134ba
10 changed files with 385 additions and 32 deletions

View file

@ -16,6 +16,9 @@
*/ */
package org.keycloak.subsystem.saml.as7; package org.keycloak.subsystem.saml.as7;
import java.util.List;
import org.jboss.as.controller.OperationFailedException;
import org.jboss.as.server.deployment.DeploymentUnit; import org.jboss.as.server.deployment.DeploymentUnit;
import org.jboss.as.web.deployment.WarMetaData; import org.jboss.as.web.deployment.WarMetaData;
import org.jboss.dmr.ModelNode; import org.jboss.dmr.ModelNode;
@ -34,19 +37,27 @@ public class Configuration {
private Configuration() { private Configuration() {
} }
void updateModel(ModelNode operation, ModelNode model) { void updateModel(ModelNode operation, ModelNode model) throws OperationFailedException {
ModelNode node = config; this.updateModel(operation, model, false);
ModelNode addr = operation.get("address");
for (Property item : addr.asPropertyList()) {
node = getNodeForAddressElement(node, item);
}
node.set(model);
} }
private ModelNode getNodeForAddressElement(ModelNode node, Property item) { void updateModel(final ModelNode operation, final ModelNode model, final boolean checkSingleton) throws OperationFailedException {
String key = item.getValue().asString(); ModelNode node = config;
ModelNode keymodel = node.get(item.getName());
return keymodel.get(key); final List<Property> addressNodes = operation.get("address").asPropertyList();
final int lastIndex = addressNodes.size() - 1;
for (int i = 0; i < addressNodes.size(); i++) {
Property addressNode = addressNodes.get(i);
// if checkSingleton is true, we verify if the key for the last element (e.g. SP or IDP) in the address path is already defined
if (i == lastIndex && checkSingleton) {
if (node.get(addressNode.getName()).isDefined()) {
// found an existing resource, throw an exception
throw new OperationFailedException("Duplicate resource: " + addressNode.getName());
}
}
node = node.get(addressNode.getName()).get(addressNode.getValue().asString());
}
node.set(model);
} }
public ModelNode getSecureDeployment(DeploymentUnit deploymentUnit) { public ModelNode getSecureDeployment(DeploymentUnit deploymentUnit) {

View file

@ -36,7 +36,7 @@ class IdentityProviderAddHandler extends AbstractAddStepHandler {
@Override @Override
protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model, ServiceVerificationHandler verificationHandler, List<ServiceController<?>> newControllers) throws OperationFailedException { protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model, ServiceVerificationHandler verificationHandler, List<ServiceController<?>> newControllers) throws OperationFailedException {
Configuration.INSTANCE.updateModel(operation, model); Configuration.INSTANCE.updateModel(operation, model, true);
} }
@Override @Override

View file

@ -76,13 +76,19 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
ModelNode addSecureDeployment = Util.createAddOperation(addr); ModelNode addSecureDeployment = Util.createAddOperation(addr);
list.add(addSecureDeployment); list.add(addSecureDeployment);
Set<String> parsedElements = new HashSet<>();
while (reader.hasNext() && nextTag(reader) != END_ELEMENT) { while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
String tagName = reader.getLocalName(); String tagName = reader.getLocalName();
if (parsedElements.contains(tagName)) {
// all sub-elements of the secure deployment type should occur only once.
throw ParseUtils.unexpectedElement(reader);
}
if (tagName.equals(Constants.XML.SERVICE_PROVIDER)) { if (tagName.equals(Constants.XML.SERVICE_PROVIDER)) {
readServiceProvider(reader, list, addr); readServiceProvider(reader, list, addr);
} else { } else {
throw ParseUtils.unexpectedElement(reader); throw ParseUtils.unexpectedElement(reader);
} }
parsedElements.add(tagName);
} }
} }
@ -109,8 +115,13 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
attr.parseAndSetParameter(value, addServiceProvider, reader); attr.parseAndSetParameter(value, addServiceProvider, reader);
} }
Set parsedElements = new HashSet<>();
while (reader.hasNext() && nextTag(reader) != END_ELEMENT) { while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
String tagName = reader.getLocalName(); String tagName = reader.getLocalName();
if (parsedElements.contains(tagName)) {
// all sub-elements of the service provider type should occur only once.
throw ParseUtils.unexpectedElement(reader);
}
if (Constants.XML.KEYS.equals(tagName)) { if (Constants.XML.KEYS.equals(tagName)) {
readKeys(list, reader, addr); readKeys(list, reader, addr);
@ -125,6 +136,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
} else { } else {
throw ParseUtils.unexpectedElement(reader); throw ParseUtils.unexpectedElement(reader);
} }
parsedElements.add(tagName);
} }
} }
@ -152,8 +164,13 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
attr.parseAndSetParameter(value, addIdentityProvider, reader); attr.parseAndSetParameter(value, addIdentityProvider, reader);
} }
Set<String> parsedElements = new HashSet<>();
while (reader.hasNext() && nextTag(reader) != END_ELEMENT) { while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
String tagName = reader.getLocalName(); String tagName = reader.getLocalName();
if (parsedElements.contains(tagName)) {
// all sub-elements of the identity provider type should occur only once.
throw ParseUtils.unexpectedElement(reader);
}
if (Constants.XML.SINGLE_SIGN_ON.equals(tagName)) { if (Constants.XML.SINGLE_SIGN_ON.equals(tagName)) {
readSingleSignOn(addIdentityProvider, reader); readSingleSignOn(addIdentityProvider, reader);
@ -168,6 +185,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
} else { } else {
throw ParseUtils.unexpectedElement(reader); throw ParseUtils.unexpectedElement(reader);
} }
parsedElements.add(tagName);
} }
} }
@ -265,8 +283,13 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
attr.parseAndSetParameter(value, addKey, reader); attr.parseAndSetParameter(value, addKey, reader);
} }
Set<String> parsedElements = new HashSet<>();
while (reader.hasNext() && nextTag(reader) != END_ELEMENT) { while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
String tagName = reader.getLocalName(); String tagName = reader.getLocalName();
if (parsedElements.contains(tagName)) {
// all sub-elements of the key type should occur only once.
throw ParseUtils.unexpectedElement(reader);
}
if (Constants.XML.KEY_STORE.equals(tagName)) { if (Constants.XML.KEY_STORE.equals(tagName)) {
readKeyStore(addKey, reader); readKeyStore(addKey, reader);
@ -278,6 +301,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
} else { } else {
throw ParseUtils.unexpectedElement(reader); throw ParseUtils.unexpectedElement(reader);
} }
parsedElements.add(tagName);
} }
} }
@ -308,8 +332,13 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
throw ParseUtils.missingRequired(reader, asSet(Constants.XML.PASSWORD)); throw ParseUtils.missingRequired(reader, asSet(Constants.XML.PASSWORD));
} }
Set<String> parsedElements = new HashSet<>();
while (reader.hasNext() && nextTag(reader) != END_ELEMENT) { while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
String tagName = reader.getLocalName(); String tagName = reader.getLocalName();
if (parsedElements.contains(tagName)) {
// all sub-elements of the keystore type should occur only once.
throw ParseUtils.unexpectedElement(reader);
}
if (Constants.XML.PRIVATE_KEY.equals(tagName)) { if (Constants.XML.PRIVATE_KEY.equals(tagName)) {
readPrivateKey(reader, addKeyStore); readPrivateKey(reader, addKeyStore);
} else if (Constants.XML.CERTIFICATE.equals(tagName)) { } else if (Constants.XML.CERTIFICATE.equals(tagName)) {
@ -317,6 +346,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
} else { } else {
throw ParseUtils.unexpectedElement(reader); throw ParseUtils.unexpectedElement(reader);
} }
parsedElements.add(tagName);
} }
} }
@ -500,9 +530,9 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
writeKeys(writer, idpAttributes.get(Constants.Model.KEY)); writeKeys(writer, idpAttributes.get(Constants.Model.KEY));
writeHttpClient(writer, idpAttributes.get(Constants.Model.HTTP_CLIENT)); writeHttpClient(writer, idpAttributes.get(Constants.Model.HTTP_CLIENT));
writeAllowedClockSkew(writer, idpAttributes.get(Constants.Model.ALLOWED_CLOCK_SKEW)); writeAllowedClockSkew(writer, idpAttributes.get(Constants.Model.ALLOWED_CLOCK_SKEW));
}
writer.writeEndElement(); writer.writeEndElement();
} }
}
void writeSingleSignOn(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException { void writeSingleSignOn(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
if (!model.isDefined()) { if (!model.isDefined()) {

View file

@ -38,7 +38,7 @@ class ServiceProviderAddHandler extends AbstractAddStepHandler {
@Override @Override
protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model, ServiceVerificationHandler verificationHandler, List<ServiceController<?>> newControllers) throws OperationFailedException { protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model, ServiceVerificationHandler verificationHandler, List<ServiceController<?>> newControllers) throws OperationFailedException {
Configuration.INSTANCE.updateModel(operation, model); Configuration.INSTANCE.updateModel(operation, model, true);
} }
@Override @Override

View file

@ -17,8 +17,34 @@
package org.keycloak.subsystem.saml.as7; package org.keycloak.subsystem.saml.as7;
import java.io.IOException; import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.jboss.as.subsystem.test.AbstractSubsystemBaseTest; import org.jboss.as.subsystem.test.AbstractSubsystemBaseTest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
/** /**
@ -28,12 +54,118 @@ import org.jboss.as.subsystem.test.AbstractSubsystemBaseTest;
*/ */
public class SubsystemParsingTestCase extends AbstractSubsystemBaseTest { public class SubsystemParsingTestCase extends AbstractSubsystemBaseTest {
private String subsystemXml = null;
private String subsystemTemplate = null;
private Document document = null;
@Rule
public final ExpectedException exception = ExpectedException.none();
public SubsystemParsingTestCase() { public SubsystemParsingTestCase() {
super(KeycloakSamlExtension.SUBSYSTEM_NAME, new KeycloakSamlExtension()); super(KeycloakSamlExtension.SUBSYSTEM_NAME, new KeycloakSamlExtension());
} }
@Override @Override
protected String getSubsystemXml() throws IOException { protected String getSubsystemXml() throws IOException {
return readResource("keycloak-saml-1.3.xml"); return this.subsystemXml;
}
@Before
public void initialize() throws IOException {
this.subsystemTemplate = readResource("keycloak-saml-1.3.xml");
try {
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
this.document = builder.parse(new InputSource(new StringReader(this.subsystemTemplate)));
} catch (ParserConfigurationException | SAXException e) {
throw new IOException(e);
}
}
private void buildSubsystemXml(final Element element, final String expression) throws IOException {
if (element != null) {
try {
// locate the element and insert the node
XPath xPath = XPathFactory.newInstance().newXPath();
NodeList nodeList = (NodeList) xPath.compile(expression).evaluate(this.document, XPathConstants.NODESET);
nodeList.item(0).appendChild(element);
// transform again to XML
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
StringWriter writer = new StringWriter();
transformer.transform(new DOMSource(this.document), new StreamResult(writer));
this.subsystemXml = writer.getBuffer().toString();
} catch(TransformerException | XPathExpressionException e) {
throw new IOException(e);
}
} else {
this.subsystemXml = this.subsystemTemplate;
}
}
@Override
public void testSubsystem() throws Exception {
this.buildSubsystemXml(null, null);
super.testSubsystem();
}
@Test
public void testDuplicateServiceProviders() throws Exception {
// create a simple service provider element.
Element spElement = this.document.createElement(Constants.XML.SERVICE_PROVIDER);
spElement.setAttribute(Constants.XML.ENTITY_ID, "duplicate-sp");
this.buildSubsystemXml(spElement, "/subsystem/secure-deployment[1]");
this.exception.expect(XMLStreamException.class);
this.exception.expectMessage("JBAS014789: Unexpected element");
super.testSubsystem();
}
@Test
public void testDuplicateIdentityProviders() throws Exception {
// create a duplicate identity provider element.
Element idpElement = this.document.createElement(Constants.XML.IDENTITY_PROVIDER);
idpElement.setAttribute(Constants.XML.ENTITY_ID, "test-idp");
Element singleSignOn = this.document.createElement(Constants.XML.SINGLE_SIGN_ON);
singleSignOn.setAttribute(Constants.XML.BINDING_URL, "https://localhost:7887");
Element singleLogout = this.document.createElement(Constants.XML.SINGLE_LOGOUT);
singleLogout.setAttribute(Constants.XML.POST_BINDING_URL, "httpsL//localhost:8998");
idpElement.appendChild(singleSignOn);
idpElement.appendChild(singleLogout);
this.buildSubsystemXml(idpElement, "/subsystem/secure-deployment[1]/SP");
this.exception.expect(XMLStreamException.class);
this.exception.expectMessage("JBAS014789: Unexpected element");
super.testSubsystem();
}
@Test
public void testDuplicateKeysInSP() throws Exception {
Element keysElement = this.document.createElement(Constants.XML.KEYS);
Element keyElement = this.document.createElement(Constants.XML.KEY);
keyElement.setAttribute(Constants.XML.ENCRYPTION, "false");
keyElement.setAttribute(Constants.XML.SIGNING, "false");
keysElement.appendChild(keyElement);
this.buildSubsystemXml(keysElement, "/subsystem/secure-deployment[1]/SP");
this.exception.expect(XMLStreamException.class);
this.exception.expectMessage("JBAS014789: Unexpected element");
super.testSubsystem();
}
@Test
public void testDuplicateKeysInIDP() throws Exception {
Element keysElement = this.document.createElement(Constants.XML.KEYS);
Element keyElement = this.document.createElement(Constants.XML.KEY);
keyElement.setAttribute(Constants.XML.ENCRYPTION, "false");
keyElement.setAttribute(Constants.XML.SIGNING, "false");
keysElement.appendChild(keyElement);
this.buildSubsystemXml(keysElement, "/subsystem/secure-deployment[1]/SP/IDP");
this.exception.expect(XMLStreamException.class);
this.exception.expectMessage("JBAS014789: Unexpected element");
super.testSubsystem();
} }
} }

View file

@ -16,6 +16,9 @@
*/ */
package org.keycloak.subsystem.adapter.saml.extension; package org.keycloak.subsystem.adapter.saml.extension;
import java.util.List;
import org.jboss.as.controller.OperationFailedException;
import org.jboss.as.server.deployment.DeploymentUnit; import org.jboss.as.server.deployment.DeploymentUnit;
import org.jboss.as.web.common.WarMetaData; import org.jboss.as.web.common.WarMetaData;
import org.jboss.dmr.ModelNode; import org.jboss.dmr.ModelNode;
@ -34,19 +37,27 @@ public class Configuration {
private Configuration() { private Configuration() {
} }
void updateModel(ModelNode operation, ModelNode model) { void updateModel(ModelNode operation, ModelNode model) throws OperationFailedException {
ModelNode node = config; this.updateModel(operation, model, false);
ModelNode addr = operation.get("address");
for (Property item : addr.asPropertyList()) {
node = getNodeForAddressElement(node, item);
}
node.set(model);
} }
private ModelNode getNodeForAddressElement(ModelNode node, Property item) { void updateModel(final ModelNode operation, final ModelNode model, final boolean checkSingleton) throws OperationFailedException {
String key = item.getValue().asString(); ModelNode node = config;
ModelNode keymodel = node.get(item.getName());
return keymodel.get(key); final List<Property> addressNodes = operation.get("address").asPropertyList();
final int lastIndex = addressNodes.size() - 1;
for (int i = 0; i < addressNodes.size(); i++) {
Property addressNode = addressNodes.get(i);
// if checkSingleton is true, we verify if the key for the last element (e.g. SP or IDP) in the address path is already defined
if (i == lastIndex && checkSingleton) {
if (node.get(addressNode.getName()).isDefined()) {
// found an existing resource, throw an exception
throw new OperationFailedException("Duplicate resource: " + addressNode.getName());
}
}
node = node.get(addressNode.getName()).get(addressNode.getValue().asString());
}
node.set(model);
} }
public ModelNode getSecureDeployment(DeploymentUnit deploymentUnit) { public ModelNode getSecureDeployment(DeploymentUnit deploymentUnit) {

View file

@ -32,6 +32,6 @@ class IdentityProviderAddHandler extends AbstractAddStepHandler {
@Override @Override
protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException {
Configuration.INSTANCE.updateModel(operation, model); Configuration.INSTANCE.updateModel(operation, model, true);
} }
} }

View file

@ -33,8 +33,10 @@ import org.jboss.staxmapper.XMLExtendedStreamWriter;
import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamException;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Set;
/** /**
* The subsystem parser, which uses stax to read and write to and from xml * The subsystem parser, which uses stax to read and write to and from xml
@ -74,13 +76,19 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
ModelNode addSecureDeployment = Util.createAddOperation(addr); ModelNode addSecureDeployment = Util.createAddOperation(addr);
list.add(addSecureDeployment); list.add(addSecureDeployment);
Set<String> parsedElements = new HashSet<>();
while (reader.hasNext() && nextTag(reader) != END_ELEMENT) { while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
String tagName = reader.getLocalName(); String tagName = reader.getLocalName();
if (parsedElements.contains(tagName)) {
// all sub-elements of the secure deployment type should occur only once.
throw ParseUtils.unexpectedElement(reader);
}
if (tagName.equals(Constants.XML.SERVICE_PROVIDER)) { if (tagName.equals(Constants.XML.SERVICE_PROVIDER)) {
readServiceProvider(reader, list, addr); readServiceProvider(reader, list, addr);
} else { } else {
throw ParseUtils.unexpectedElement(reader); throw ParseUtils.unexpectedElement(reader);
} }
parsedElements.add(tagName);
} }
} }
@ -107,9 +115,13 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
attr.parseAndSetParameter(value, addServiceProvider, reader); attr.parseAndSetParameter(value, addServiceProvider, reader);
} }
Set<String> parsedElements = new HashSet<>();
while (reader.hasNext() && nextTag(reader) != END_ELEMENT) { while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
String tagName = reader.getLocalName(); String tagName = reader.getLocalName();
if (parsedElements.contains(tagName)) {
// all sub-elements of the service provider type should occur only once.
throw ParseUtils.unexpectedElement(reader);
}
if (Constants.XML.KEYS.equals(tagName)) { if (Constants.XML.KEYS.equals(tagName)) {
readKeys(list, reader, addr); readKeys(list, reader, addr);
} else if (Constants.XML.PRINCIPAL_NAME_MAPPING.equals(tagName)) { } else if (Constants.XML.PRINCIPAL_NAME_MAPPING.equals(tagName)) {
@ -123,6 +135,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
} else { } else {
throw ParseUtils.unexpectedElement(reader); throw ParseUtils.unexpectedElement(reader);
} }
parsedElements.add(tagName);
} }
} }
@ -150,8 +163,13 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
attr.parseAndSetParameter(value, addIdentityProvider, reader); attr.parseAndSetParameter(value, addIdentityProvider, reader);
} }
Set<String> parsedElements = new HashSet<>();
while (reader.hasNext() && nextTag(reader) != END_ELEMENT) { while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
String tagName = reader.getLocalName(); String tagName = reader.getLocalName();
if (parsedElements.contains(tagName)) {
// all sub-elements of the identity provider type should occur only once.
throw ParseUtils.unexpectedElement(reader);
}
if (Constants.XML.SINGLE_SIGN_ON.equals(tagName)) { if (Constants.XML.SINGLE_SIGN_ON.equals(tagName)) {
readSingleSignOn(addIdentityProvider, reader); readSingleSignOn(addIdentityProvider, reader);
@ -166,6 +184,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
} else { } else {
throw ParseUtils.unexpectedElement(reader); throw ParseUtils.unexpectedElement(reader);
} }
parsedElements.add(tagName);
} }
} }
@ -263,8 +282,13 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
attr.parseAndSetParameter(value, addKey, reader); attr.parseAndSetParameter(value, addKey, reader);
} }
Set<String> parsedElements = new HashSet<>();
while (reader.hasNext() && nextTag(reader) != END_ELEMENT) { while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
String tagName = reader.getLocalName(); String tagName = reader.getLocalName();
if (parsedElements.contains(tagName)) {
// all sub-elements of the key type should occur only once.
throw ParseUtils.unexpectedElement(reader);
}
if (Constants.XML.KEY_STORE.equals(tagName)) { if (Constants.XML.KEY_STORE.equals(tagName)) {
readKeyStore(addKey, reader); readKeyStore(addKey, reader);
@ -276,6 +300,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
} else { } else {
throw ParseUtils.unexpectedElement(reader); throw ParseUtils.unexpectedElement(reader);
} }
parsedElements.add(tagName);
} }
} }
@ -306,8 +331,13 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
throw ParseUtils.missingRequired(reader, Constants.XML.PASSWORD); throw ParseUtils.missingRequired(reader, Constants.XML.PASSWORD);
} }
Set<String> parsedElements = new HashSet<>();
while (reader.hasNext() && nextTag(reader) != END_ELEMENT) { while (reader.hasNext() && nextTag(reader) != END_ELEMENT) {
String tagName = reader.getLocalName(); String tagName = reader.getLocalName();
if (parsedElements.contains(tagName)) {
// all sub-elements of the keystore type should occur only once.
throw ParseUtils.unexpectedElement(reader);
}
if (Constants.XML.PRIVATE_KEY.equals(tagName)) { if (Constants.XML.PRIVATE_KEY.equals(tagName)) {
readPrivateKey(reader, addKeyStore); readPrivateKey(reader, addKeyStore);
} else if (Constants.XML.CERTIFICATE.equals(tagName)) { } else if (Constants.XML.CERTIFICATE.equals(tagName)) {
@ -315,6 +345,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
} else { } else {
throw ParseUtils.unexpectedElement(reader); throw ParseUtils.unexpectedElement(reader);
} }
parsedElements.add(tagName);
} }
} }
@ -498,9 +529,9 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
writeKeys(writer, idpAttributes.get(Constants.Model.KEY)); writeKeys(writer, idpAttributes.get(Constants.Model.KEY));
writeHttpClient(writer, idpAttributes.get(Constants.Model.HTTP_CLIENT)); writeHttpClient(writer, idpAttributes.get(Constants.Model.HTTP_CLIENT));
writeAllowedClockSkew(writer, idpAttributes.get(Constants.Model.ALLOWED_CLOCK_SKEW)); writeAllowedClockSkew(writer, idpAttributes.get(Constants.Model.ALLOWED_CLOCK_SKEW));
}
writer.writeEndElement(); writer.writeEndElement();
} }
}
void writeSingleSignOn(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException { void writeSingleSignOn(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
if (!model.isDefined()) { if (!model.isDefined()) {

View file

@ -34,6 +34,6 @@ class ServiceProviderAddHandler extends AbstractAddStepHandler {
@Override @Override
protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException { protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) throws OperationFailedException {
Configuration.INSTANCE.updateModel(operation, model); Configuration.INSTANCE.updateModel(operation, model, true);
} }
} }

View file

@ -17,14 +17,40 @@
package org.keycloak.subsystem.adapter.saml.extension; package org.keycloak.subsystem.adapter.saml.extension;
import org.jboss.as.subsystem.test.AbstractSubsystemBaseTest; import org.jboss.as.subsystem.test.AbstractSubsystemBaseTest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import java.io.IOException; import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.stream.XMLStreamException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
/** /**
* Tests all management expects for subsystem, parsing, marshaling, model definition and other * Tests all management expects for subsystem, parsing, marshaling, model definition and other
* Here is an example that allows you a fine grained controller over what is tested and how. So it can give you ideas what can be done and tested. * Here is an example that allows you a fine grained controller over what is tested and how. So it can give you ideas what can be done and tested.
* If you have no need for advanced testing of subsystem you look at {@link SubsystemBaseParsingTestCase} that testes same stuff but most of the code * If you have no need for advanced testing of subsystem you look at {@link AbstractSubsystemBaseTest} that testes same stuff but most of the code
* is hidden inside of test harness * is hidden inside of test harness
* *
* @author <a href="kabir.khan@jboss.com">Kabir Khan</a> * @author <a href="kabir.khan@jboss.com">Kabir Khan</a>
@ -33,13 +59,22 @@ import java.io.IOException;
*/ */
public class SubsystemParsingTestCase extends AbstractSubsystemBaseTest { public class SubsystemParsingTestCase extends AbstractSubsystemBaseTest {
private String subsystemXml = null;
private String subsystemTemplate = null;
private Document document = null;
@Rule
public final ExpectedException exception = ExpectedException.none();
public SubsystemParsingTestCase() { public SubsystemParsingTestCase() {
super(KeycloakSamlExtension.SUBSYSTEM_NAME, new KeycloakSamlExtension()); super(KeycloakSamlExtension.SUBSYSTEM_NAME, new KeycloakSamlExtension());
} }
@Override @Override
protected String getSubsystemXml() throws IOException { protected String getSubsystemXml() throws IOException {
return readResource("keycloak-saml-1.3.xml"); return this.subsystemXml;
} }
@Override @Override
@ -53,4 +88,107 @@ public class SubsystemParsingTestCase extends AbstractSubsystemBaseTest {
"/subsystem-templates/keycloak-saml-adapter.xml" "/subsystem-templates/keycloak-saml-adapter.xml"
}; };
} }
@Before
public void initialize() throws IOException {
this.subsystemTemplate = readResource("keycloak-saml-1.3.xml");
try {
DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
this.document = builder.parse(new InputSource(new StringReader(this.subsystemTemplate)));
} catch (ParserConfigurationException | SAXException e) {
throw new IOException(e);
}
}
private void buildSubsystemXml(final Element element, final String expression) throws IOException {
if (element != null) {
try {
// locate the element and insert the node
XPath xPath = XPathFactory.newInstance().newXPath();
NodeList nodeList = (NodeList) xPath.compile(expression).evaluate(this.document, XPathConstants.NODESET);
nodeList.item(0).appendChild(element);
// transform again to XML
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
StringWriter writer = new StringWriter();
transformer.transform(new DOMSource(this.document), new StreamResult(writer));
this.subsystemXml = writer.getBuffer().toString();
} catch(TransformerException | XPathExpressionException e) {
throw new IOException(e);
}
} else {
this.subsystemXml = this.subsystemTemplate;
}
}
@Override
public void testSubsystem() throws Exception {
this.buildSubsystemXml(null, null);
super.testSubsystem();
}
@Override
public void testSchema() throws Exception {
this.buildSubsystemXml(null, null);
super.testSchema();
}
@Test
public void testDuplicateServiceProviders() throws Exception {
// create a simple service provider element.
Element spElement = this.document.createElement(Constants.XML.SERVICE_PROVIDER);
spElement.setAttribute(Constants.XML.ENTITY_ID, "duplicate-sp");
this.buildSubsystemXml(spElement, "/subsystem/secure-deployment[1]");
this.exception.expect(XMLStreamException.class);
this.exception.expectMessage("WFLYCTL0198: Unexpected element");
super.testSubsystem();
}
@Test
public void testDuplicateIdentityProviders() throws Exception {
// create a duplicate identity provider element.
Element idpElement = this.document.createElement(Constants.XML.IDENTITY_PROVIDER);
idpElement.setAttribute(Constants.XML.ENTITY_ID, "test-idp");
Element singleSignOn = this.document.createElement(Constants.XML.SINGLE_SIGN_ON);
singleSignOn.setAttribute(Constants.XML.BINDING_URL, "https://localhost:7887");
Element singleLogout = this.document.createElement(Constants.XML.SINGLE_LOGOUT);
singleLogout.setAttribute(Constants.XML.POST_BINDING_URL, "httpsL//localhost:8998");
idpElement.appendChild(singleSignOn);
idpElement.appendChild(singleLogout);
this.buildSubsystemXml(idpElement, "/subsystem/secure-deployment[1]/SP");
this.exception.expect(XMLStreamException.class);
this.exception.expectMessage("WFLYCTL0198: Unexpected element");
super.testSubsystem();
}
@Test
public void testDuplicateKeysInSP() throws Exception {
Element keysElement = this.document.createElement(Constants.XML.KEYS);
Element keyElement = this.document.createElement(Constants.XML.KEY);
keyElement.setAttribute(Constants.XML.ENCRYPTION, "false");
keyElement.setAttribute(Constants.XML.SIGNING, "false");
keysElement.appendChild(keyElement);
this.buildSubsystemXml(keysElement, "/subsystem/secure-deployment[1]/SP");
this.exception.expect(XMLStreamException.class);
this.exception.expectMessage("WFLYCTL0198: Unexpected element");
super.testSubsystem();
}
@Test
public void testDuplicateKeysInIDP() throws Exception {
Element keysElement = this.document.createElement(Constants.XML.KEYS);
Element keyElement = this.document.createElement(Constants.XML.KEY);
keyElement.setAttribute(Constants.XML.ENCRYPTION, "false");
keyElement.setAttribute(Constants.XML.SIGNING, "false");
keysElement.appendChild(keyElement);
this.buildSubsystemXml(keysElement, "/subsystem/secure-deployment[1]/SP/IDP");
this.exception.expect(XMLStreamException.class);
this.exception.expectMessage("WFLYCTL0198: Unexpected element");
super.testSubsystem();
}
} }