KEYCLOAK-10975: Clock skew configuration in keycloak-saml.xml can't be found in the keycloak-saml subsystem
This commit is contained in:
parent
1cdc5e1969
commit
6283c7add3
14 changed files with 700 additions and 2 deletions
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* 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 org.keycloak.subsystem.saml.as7;
|
||||
|
||||
import org.jboss.as.controller.SimpleAttributeDefinition;
|
||||
import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
|
||||
import org.jboss.as.controller.operations.validation.EnumValidator;
|
||||
import org.jboss.as.controller.operations.validation.IntRangeValidator;
|
||||
import org.jboss.dmr.ModelNode;
|
||||
import org.jboss.dmr.ModelType;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
abstract public class AllowedClockSkew {
|
||||
|
||||
static final SimpleAttributeDefinition ALLOWED_CLOCK_SKEW_VALUE =
|
||||
new SimpleAttributeDefinitionBuilder(Constants.Model.ALLOWED_CLOCK_SKEW_VALUE, ModelType.INT, false)
|
||||
.setXmlName(Constants.XML.ALLOWED_CLOCK_SKEW)
|
||||
.setAllowExpression(false)
|
||||
.setValidator(new IntRangeValidator(1, Integer.MAX_VALUE, true, false))
|
||||
.build();
|
||||
|
||||
static private enum AllowedClockSkewUnits {MINUTES, SECONDS, MILLISECONDS, MICROSECONDS, NANOSECONDS};
|
||||
|
||||
static final SimpleAttributeDefinition ALLOWED_CLOCK_SKEW_UNIT =
|
||||
new SimpleAttributeDefinitionBuilder(Constants.Model.ALLOWED_CLOCK_SKEW_UNIT, ModelType.STRING, true)
|
||||
.setXmlName(Constants.XML.ALLOWED_CLOCK_SKEW_UNIT)
|
||||
.setAllowExpression(false)
|
||||
.setDefaultValue(new ModelNode(AllowedClockSkewUnits.SECONDS.name()))
|
||||
.setValidator(EnumValidator.create(AllowedClockSkewUnits.class, true, false))
|
||||
.build();
|
||||
|
||||
static final SimpleAttributeDefinition[] ATTRIBUTES = {ALLOWED_CLOCK_SKEW_UNIT, ALLOWED_CLOCK_SKEW_VALUE};
|
||||
}
|
|
@ -72,6 +72,10 @@ public class Constants {
|
|||
|
||||
static final String ROLE_MAPPINGS_PROVIDER_ID = "roleMappingsProviderId";
|
||||
static final String ROLE_MAPPINGS_PROVIDER_CONFIG = "roleMappingsProviderConfig";
|
||||
|
||||
static final String ALLOWED_CLOCK_SKEW = "AllowedClockSkew";
|
||||
static final String ALLOWED_CLOCK_SKEW_UNIT = "unit";
|
||||
static final String ALLOWED_CLOCK_SKEW_VALUE = "value";
|
||||
}
|
||||
|
||||
static class XML {
|
||||
|
@ -133,5 +137,8 @@ public class Constants {
|
|||
static final String VALUE = "value";
|
||||
static final String PROPERTY = "Property";
|
||||
static final String ROLE_MAPPINGS_PROVIDER = "RoleMappingsProvider";
|
||||
|
||||
static final String ALLOWED_CLOCK_SKEW = "AllowedClockSkew";
|
||||
static final String ALLOWED_CLOCK_SKEW_UNIT = "unit";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,9 +63,15 @@ public class IdentityProviderDefinition extends SimpleResourceDefinition {
|
|||
.setAllowNull(false)
|
||||
.build();
|
||||
|
||||
static final ObjectTypeAttributeDefinition ALLOWED_CLOCK_SKEW =
|
||||
ObjectTypeAttributeDefinition.Builder.of(Constants.Model.ALLOWED_CLOCK_SKEW,
|
||||
AllowedClockSkew.ATTRIBUTES)
|
||||
.setAllowNull(true)
|
||||
.build();
|
||||
|
||||
static final SimpleAttributeDefinition[] ATTRIBUTES = {SIGNATURES_REQUIRED, SIGNATURE_ALGORITHM, SIGNATURE_CANONICALIZATION_METHOD};
|
||||
|
||||
static final SimpleAttributeDefinition[] ALL_ATTRIBUTES = {SIGNATURES_REQUIRED, SIGNATURE_ALGORITHM, SIGNATURE_CANONICALIZATION_METHOD, SINGLE_SIGN_ON, SINGLE_LOGOUT};
|
||||
static final SimpleAttributeDefinition[] ALL_ATTRIBUTES = {SIGNATURES_REQUIRED, SIGNATURE_ALGORITHM, SIGNATURE_CANONICALIZATION_METHOD, SINGLE_SIGN_ON, SINGLE_LOGOUT, ALLOWED_CLOCK_SKEW};
|
||||
|
||||
static final HashMap<String, SimpleAttributeDefinition> ATTRIBUTE_MAP = new HashMap<>();
|
||||
|
||||
|
|
|
@ -161,6 +161,8 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
|||
readSingleLogout(addIdentityProvider, reader);
|
||||
} else if (Constants.XML.KEYS.equals(tagName)) {
|
||||
readKeys(list, reader, addr);
|
||||
} else if (Constants.XML.ALLOWED_CLOCK_SKEW.equals(tagName)) {
|
||||
readAllowedClockSkew(addIdentityProvider, reader);
|
||||
} else {
|
||||
throw ParseUtils.unexpectedElement(reader);
|
||||
}
|
||||
|
@ -210,6 +212,25 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
|||
list.addAll(keyList);
|
||||
}
|
||||
|
||||
void readAllowedClockSkew(ModelNode addIdentityProvider, XMLExtendedStreamReader reader) throws XMLStreamException {
|
||||
ModelNode allowedClockSkew = addIdentityProvider.get(Constants.Model.ALLOWED_CLOCK_SKEW);
|
||||
for (int i = 0; i < reader.getAttributeCount(); i++) {
|
||||
String name = reader.getAttributeLocalName(i);
|
||||
String value = reader.getAttributeValue(i);
|
||||
|
||||
if (Constants.XML.ALLOWED_CLOCK_SKEW_UNIT.equals(name)) {
|
||||
SimpleAttributeDefinition attr = AllowedClockSkew.ALLOWED_CLOCK_SKEW_UNIT;
|
||||
attr.parseAndSetParameter(value, allowedClockSkew, reader);
|
||||
} else {
|
||||
throw ParseUtils.unexpectedAttribute(reader, i);
|
||||
}
|
||||
}
|
||||
// the real value is the content
|
||||
String value = reader.getElementText();
|
||||
SimpleAttributeDefinition attr = AllowedClockSkew.ALLOWED_CLOCK_SKEW_VALUE;
|
||||
attr.parseAndSetParameter(value, allowedClockSkew, reader);
|
||||
}
|
||||
|
||||
void readKey(List<ModelNode> list, XMLExtendedStreamReader reader, PathAddress parentAddr) throws XMLStreamException {
|
||||
PathAddress addr = PathAddress.pathAddress(parentAddr,
|
||||
PathElement.pathElement(Constants.Model.KEY, "key-" + list.size()));
|
||||
|
@ -460,6 +481,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
|||
writeSingleSignOn(writer, idpAttributes.get(Constants.Model.SINGLE_SIGN_ON));
|
||||
writeSingleLogout(writer, idpAttributes.get(Constants.Model.SINGLE_LOGOUT));
|
||||
writeKeys(writer, idpAttributes.get(Constants.Model.KEY));
|
||||
writeAllowedClockSkew(writer, idpAttributes.get(Constants.Model.ALLOWED_CLOCK_SKEW));
|
||||
}
|
||||
writer.writeEndElement();
|
||||
}
|
||||
|
@ -514,6 +536,18 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
|||
}
|
||||
}
|
||||
|
||||
void writeAllowedClockSkew(XMLExtendedStreamWriter writer, ModelNode allowedClockSkew) throws XMLStreamException {
|
||||
if (!allowedClockSkew.isDefined()) {
|
||||
return;
|
||||
}
|
||||
writer.writeStartElement(Constants.XML.ALLOWED_CLOCK_SKEW);
|
||||
AllowedClockSkew.ALLOWED_CLOCK_SKEW_UNIT.getAttributeMarshaller().marshallAsAttribute(AllowedClockSkew.ALLOWED_CLOCK_SKEW_UNIT, allowedClockSkew, false, writer);
|
||||
ModelNode allowedClockSkewValue = allowedClockSkew.get(Constants.Model.ALLOWED_CLOCK_SKEW_VALUE);
|
||||
char[] chars = allowedClockSkewValue.asString().toCharArray();
|
||||
writer.writeCharacters(chars, 0, chars.length);
|
||||
writer.writeEndElement();
|
||||
}
|
||||
|
||||
void writeKeyStore(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
|
||||
if (!model.isDefined()) {
|
||||
return;
|
||||
|
|
|
@ -86,3 +86,6 @@ keycloak-saml.IDP.SingleLogoutService.responseBinding=HTTP method to use for res
|
|||
keycloak-saml.IDP.SingleLogoutService.postBindingUrl=Endpoint URL for posting
|
||||
keycloak-saml.IDP.SingleLogoutService.redirectBindingUrl=Endpoint URL for redirects
|
||||
keycloak-saml.IDP.Key=Key definition for identity provider
|
||||
keycloak-saml.IDP.AllowedClockSkew=Allowed clock skew between the IDP and the SP
|
||||
keycloak-saml.IDP.AllowedClockSkew.value=Allowed clock skew value between the IDP and the SP
|
||||
keycloak-saml.IDP.AllowedClockSkew.unit=Time unit for the value of the clock skew. Values: MINUTES, SECONDS, MILLISECONDS, MICROSECONDS, NANOSECONDS
|
||||
|
|
|
@ -105,6 +105,11 @@
|
|||
<xs:element name="SingleSignOnService" minOccurs="1" maxOccurs="1" type="single-signon-type"/>
|
||||
<xs:element name="SingleLogoutService" minOccurs="0" maxOccurs="1" type="single-logout-type"/>
|
||||
<xs:element name="Keys" minOccurs="0" maxOccurs="1" type="keys-type"/>
|
||||
<xs:element name="AllowedClockSkew" type="allowed-clock-skew-type" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>This defines the allowed clock skew between IDP and SP in milliseconds. The default value is 0.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="entityID" type="xs:string" use="required">
|
||||
<xs:annotation>
|
||||
|
@ -336,4 +341,28 @@
|
|||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="allowed-clock-skew-type">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The value is the allowed clock skew between the IDP and the SP.</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:simpleContent>
|
||||
<xs:extension base="xs:positiveInteger">
|
||||
<xs:attribute name="unit" type="clock-skew-unit-type"/>
|
||||
</xs:extension>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
<xs:simpleType name="clock-skew-unit-type">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Time unit for the value of the clock skew.</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="MINUTES" />
|
||||
<xs:enumeration value="SECONDS" />
|
||||
<xs:enumeration value="MILLISECONDS" />
|
||||
<xs:enumeration value="MICROSECONDS" />
|
||||
<xs:enumeration value="NANOSECONDS" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
</xs:schema>
|
||||
|
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* 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 org.keycloak.subsystem.saml.as7;
|
||||
|
||||
import org.jboss.as.subsystem.test.AbstractSubsystemBaseTest;
|
||||
|
||||
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.controller.ExpressionResolver;
|
||||
import org.jboss.as.controller.PathAddress;
|
||||
import org.jboss.as.controller.PathElement;
|
||||
import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
|
||||
import org.jboss.as.model.test.ModelTestUtils;
|
||||
import org.jboss.as.subsystem.test.KernelServices;
|
||||
import org.jboss.dmr.ModelNode;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.w3c.dom.DOMException;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Test case for AllowedClockSkew subsystem configuration.
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class SubsystemParsingAllowedClockSkewTestCase extends AbstractSubsystemBaseTest {
|
||||
|
||||
private String subsystemXml = null;
|
||||
|
||||
@Rule
|
||||
public final ExpectedException exception = ExpectedException.none();
|
||||
|
||||
public SubsystemParsingAllowedClockSkewTestCase() {
|
||||
super(KeycloakSamlExtension.SUBSYSTEM_NAME, new KeycloakSamlExtension());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getSubsystemXml() throws IOException {
|
||||
return subsystemXml;
|
||||
}
|
||||
|
||||
private void setSubsystemXml(String value, String unit) throws IOException {
|
||||
try {
|
||||
String template = readResource("keycloak-saml-1.2.xml");
|
||||
if (value != null) {
|
||||
// assign the AllowedClockSkew element using DOM
|
||||
DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
||||
Document doc = db.parse(new InputSource(new StringReader(template)));
|
||||
// create the skew element
|
||||
Element allowedClockSkew = doc.createElement(Constants.XML.ALLOWED_CLOCK_SKEW);
|
||||
if (unit != null) {
|
||||
allowedClockSkew.setAttribute(Constants.XML.ALLOWED_CLOCK_SKEW_UNIT, unit);
|
||||
}
|
||||
allowedClockSkew.setTextContent(value);
|
||||
// locate the IDP and insert the node
|
||||
XPath xPath = XPathFactory.newInstance().newXPath();
|
||||
NodeList nodeList = (NodeList) xPath.compile("/subsystem/secure-deployment[1]/SP/IDP").evaluate(doc, XPathConstants.NODESET);
|
||||
nodeList.item(0).appendChild(allowedClockSkew);
|
||||
// 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(doc), new StreamResult(writer));
|
||||
subsystemXml = writer.getBuffer().toString();
|
||||
} else {
|
||||
subsystemXml = template;
|
||||
}
|
||||
} catch (DOMException | ParserConfigurationException | SAXException | TransformerException | XPathExpressionException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private PathAddress getIdpPath() {
|
||||
return PathAddress.EMPTY_ADDRESS
|
||||
.append(PathElement.pathElement(ModelDescriptionConstants.SUBSYSTEM, KeycloakSamlExtension.SUBSYSTEM_NAME))
|
||||
.append(PathElement.pathElement(Constants.Model.SECURE_DEPLOYMENT, "my-app.war"))
|
||||
.append(PathElement.pathElement(Constants.Model.SERVICE_PROVIDER, "http://localhost:8080/sales-post-enc/"))
|
||||
.append(PathElement.pathElement(Constants.Model.IDENTITY_PROVIDER, "idp"));
|
||||
}
|
||||
|
||||
private void testSubsystem(String value, String unit, int realValue, String realUnit) throws Exception {
|
||||
setSubsystemXml(value, unit);
|
||||
// perform the common test
|
||||
KernelServices s = super.standardSubsystemTest(null, true);
|
||||
// get the values for the AllowedClockSkew parameters
|
||||
ModelNode idp = ModelTestUtils.getSubModel(s.readWholeModel(), getIdpPath());
|
||||
ModelNode allowedClockSkew = idp.get(Constants.Model.ALLOWED_CLOCK_SKEW);
|
||||
if (value != null) {
|
||||
Assert.assertTrue(allowedClockSkew.isDefined());
|
||||
ModelNode allowedClockSkewValue = allowedClockSkew.get(Constants.Model.ALLOWED_CLOCK_SKEW_VALUE);
|
||||
ModelNode allowedClockSkewUnit = allowedClockSkew.get(Constants.Model.ALLOWED_CLOCK_SKEW_UNIT);
|
||||
allowedClockSkewValue = ExpressionResolver.TEST_RESOLVER.resolveExpressions(allowedClockSkewValue);
|
||||
allowedClockSkewUnit = ExpressionResolver.TEST_RESOLVER.resolveExpressions(allowedClockSkewUnit);
|
||||
Assert.assertEquals(realValue, allowedClockSkewValue.asInt());
|
||||
if (unit != null) {
|
||||
Assert.assertEquals(realUnit, allowedClockSkewUnit.asString());
|
||||
} else {
|
||||
Assert.assertFalse(allowedClockSkewUnit.isDefined());
|
||||
}
|
||||
} else {
|
||||
Assert.assertFalse(allowedClockSkew.isDefined());
|
||||
}
|
||||
}
|
||||
|
||||
private void testSubsystem(String value, String unit) throws Exception {
|
||||
testSubsystem(value, unit, value == null? -1 : Integer.parseInt(value.trim()), unit);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Override
|
||||
public void testSubsystem() throws Exception {
|
||||
testSubsystem(null, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubsystemAllowedClockSkewWithUnit() throws Exception {
|
||||
testSubsystem("3500", "MILLISECONDS");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubsystemAllowedClockSkewWithoutUnit() throws Exception {
|
||||
testSubsystem("1", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubsystemAllowedClockSkewWithSpaces() throws Exception {
|
||||
testSubsystem("\n 20 \n ", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorOnNonInteger() throws Exception {
|
||||
exception.expect(NumberFormatException.class);
|
||||
//exception.expectMessage("WFLYCTL0097");
|
||||
testSubsystem("invalid-value", null, -1, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorOnNonPositiveInteger() throws Exception {
|
||||
exception.expect(XMLStreamException.class);
|
||||
exception.expectMessage("JBAS014708");
|
||||
testSubsystem("0", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorNoValidUnit() throws Exception {
|
||||
exception.expect(XMLStreamException.class);
|
||||
exception.expectMessage("JBAS014839");
|
||||
testSubsystem("30", "invalid-unit");
|
||||
}
|
||||
|
||||
// For the moment no expressions allowed as the rest of the subsystem doesn't resolve expressions
|
||||
//@Test
|
||||
//public void testExpression() throws Exception {
|
||||
// System.setProperty("test.prop.SKEW_TIME", "30");
|
||||
// System.setProperty("test.prop.SKEW_UNIT", "MILLISECONDS");
|
||||
// try {
|
||||
// testSubsystem("${test.prop.SKEW_TIME}", "${test.prop.SKEW_UNIT}", 30, "MILLISECONDS");
|
||||
// } finally {
|
||||
// System.clearProperty("test.prop.SKEW_TIME");
|
||||
// System.clearProperty("test.prop.SKEW_UNIT");
|
||||
// }
|
||||
//}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* 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 org.keycloak.subsystem.adapter.saml.extension;
|
||||
|
||||
import org.jboss.as.controller.SimpleAttributeDefinition;
|
||||
import org.jboss.as.controller.SimpleAttributeDefinitionBuilder;
|
||||
import org.jboss.as.controller.operations.validation.EnumValidator;
|
||||
import org.jboss.as.controller.operations.validation.IntRangeValidator;
|
||||
import org.jboss.dmr.ModelNode;
|
||||
import org.jboss.dmr.ModelType;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
abstract public class AllowedClockSkew {
|
||||
|
||||
static final SimpleAttributeDefinition ALLOWED_CLOCK_SKEW_VALUE =
|
||||
new SimpleAttributeDefinitionBuilder(Constants.Model.ALLOWED_CLOCK_SKEW_VALUE, ModelType.INT, false)
|
||||
.setXmlName(Constants.XML.ALLOWED_CLOCK_SKEW)
|
||||
.setAllowExpression(false)
|
||||
.setValidator(new IntRangeValidator(1, Integer.MAX_VALUE, true, false))
|
||||
.build();
|
||||
|
||||
static private enum AllowedClockSkewUnits {MINUTES, SECONDS, MILLISECONDS, MICROSECONDS, NANOSECONDS};
|
||||
|
||||
static final SimpleAttributeDefinition ALLOWED_CLOCK_SKEW_UNIT =
|
||||
new SimpleAttributeDefinitionBuilder(Constants.Model.ALLOWED_CLOCK_SKEW_UNIT, ModelType.STRING, true)
|
||||
.setXmlName(Constants.XML.ALLOWED_CLOCK_SKEW_UNIT)
|
||||
.setAllowExpression(false)
|
||||
.setDefaultValue(new ModelNode(AllowedClockSkewUnits.SECONDS.name()))
|
||||
.setAllowedValues(AllowedClockSkewUnits.MINUTES.name(), AllowedClockSkewUnits.SECONDS.name(),
|
||||
AllowedClockSkewUnits.MILLISECONDS.name(), AllowedClockSkewUnits.MICROSECONDS.name(),
|
||||
AllowedClockSkewUnits.NANOSECONDS.name())
|
||||
.setValidator(EnumValidator.create(AllowedClockSkewUnits.class, true, false))
|
||||
.build();
|
||||
|
||||
static final SimpleAttributeDefinition[] ATTRIBUTES = {ALLOWED_CLOCK_SKEW_UNIT, ALLOWED_CLOCK_SKEW_VALUE};
|
||||
}
|
|
@ -74,6 +74,10 @@ public class Constants {
|
|||
|
||||
static final String ROLE_MAPPINGS_PROVIDER_ID = "roleMappingsProviderId";
|
||||
static final String ROLE_MAPPINGS_PROVIDER_CONFIG = "roleMappingsProviderConfig";
|
||||
|
||||
static final String ALLOWED_CLOCK_SKEW = "AllowedClockSkew";
|
||||
static final String ALLOWED_CLOCK_SKEW_UNIT = "unit";
|
||||
static final String ALLOWED_CLOCK_SKEW_VALUE = "value";
|
||||
}
|
||||
|
||||
|
||||
|
@ -136,6 +140,9 @@ public class Constants {
|
|||
static final String VALUE = "value";
|
||||
static final String PROPERTY = "Property";
|
||||
static final String ROLE_MAPPINGS_PROVIDER = "RoleMappingsProvider";
|
||||
|
||||
static final String ALLOWED_CLOCK_SKEW = "AllowedClockSkew";
|
||||
static final String ALLOWED_CLOCK_SKEW_UNIT = "unit";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -63,9 +63,15 @@ public class IdentityProviderDefinition extends SimpleResourceDefinition {
|
|||
.setAllowNull(false)
|
||||
.build();
|
||||
|
||||
static final ObjectTypeAttributeDefinition ALLOWED_CLOCK_SKEW =
|
||||
ObjectTypeAttributeDefinition.Builder.of(Constants.Model.ALLOWED_CLOCK_SKEW,
|
||||
AllowedClockSkew.ATTRIBUTES)
|
||||
.setAllowNull(true)
|
||||
.build();
|
||||
|
||||
static final SimpleAttributeDefinition[] ATTRIBUTES = {SIGNATURES_REQUIRED, SIGNATURE_ALGORITHM, SIGNATURE_CANONICALIZATION_METHOD};
|
||||
|
||||
static final SimpleAttributeDefinition[] ALL_ATTRIBUTES = {SIGNATURES_REQUIRED, SIGNATURE_ALGORITHM, SIGNATURE_CANONICALIZATION_METHOD, SINGLE_SIGN_ON, SINGLE_LOGOUT};
|
||||
static final SimpleAttributeDefinition[] ALL_ATTRIBUTES = {SIGNATURES_REQUIRED, SIGNATURE_ALGORITHM, SIGNATURE_CANONICALIZATION_METHOD, SINGLE_SIGN_ON, SINGLE_LOGOUT, ALLOWED_CLOCK_SKEW};
|
||||
|
||||
static final HashMap<String, SimpleAttributeDefinition> ATTRIBUTE_MAP = new HashMap<>();
|
||||
|
||||
|
|
|
@ -159,6 +159,8 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
|||
readSingleLogout(addIdentityProvider, reader);
|
||||
} else if (Constants.XML.KEYS.equals(tagName)) {
|
||||
readKeys(list, reader, addr);
|
||||
} else if (Constants.XML.ALLOWED_CLOCK_SKEW.equals(tagName)) {
|
||||
readAllowedClockSkew(addIdentityProvider, reader);
|
||||
} else {
|
||||
throw ParseUtils.unexpectedElement(reader);
|
||||
}
|
||||
|
@ -208,6 +210,25 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
|||
list.addAll(keyList);
|
||||
}
|
||||
|
||||
void readAllowedClockSkew(ModelNode addIdentityProvider, XMLExtendedStreamReader reader) throws XMLStreamException {
|
||||
ModelNode allowedClockSkew = addIdentityProvider.get(Constants.Model.ALLOWED_CLOCK_SKEW);
|
||||
for (int i = 0; i < reader.getAttributeCount(); i++) {
|
||||
String name = reader.getAttributeLocalName(i);
|
||||
String value = reader.getAttributeValue(i);
|
||||
|
||||
if (Constants.XML.ALLOWED_CLOCK_SKEW_UNIT.equals(name)) {
|
||||
SimpleAttributeDefinition attr = AllowedClockSkew.ALLOWED_CLOCK_SKEW_UNIT;
|
||||
attr.parseAndSetParameter(value, allowedClockSkew, reader);
|
||||
} else {
|
||||
throw ParseUtils.unexpectedAttribute(reader, i);
|
||||
}
|
||||
}
|
||||
// the real value is the content
|
||||
String value = reader.getElementText();
|
||||
SimpleAttributeDefinition attr = AllowedClockSkew.ALLOWED_CLOCK_SKEW_VALUE;
|
||||
attr.parseAndSetParameter(value, allowedClockSkew, reader);
|
||||
}
|
||||
|
||||
void readKey(List<ModelNode> list, XMLExtendedStreamReader reader, PathAddress parentAddr) throws XMLStreamException {
|
||||
PathAddress addr = PathAddress.pathAddress(parentAddr,
|
||||
PathElement.pathElement(Constants.Model.KEY, "key-" + list.size()));
|
||||
|
@ -458,6 +479,7 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
|||
writeSingleSignOn(writer, idpAttributes.get(Constants.Model.SINGLE_SIGN_ON));
|
||||
writeSingleLogout(writer, idpAttributes.get(Constants.Model.SINGLE_LOGOUT));
|
||||
writeKeys(writer, idpAttributes.get(Constants.Model.KEY));
|
||||
writeAllowedClockSkew(writer, idpAttributes.get(Constants.Model.ALLOWED_CLOCK_SKEW));
|
||||
}
|
||||
writer.writeEndElement();
|
||||
}
|
||||
|
@ -512,6 +534,18 @@ class KeycloakSubsystemParser implements XMLStreamConstants, XMLElementReader<Li
|
|||
}
|
||||
}
|
||||
|
||||
void writeAllowedClockSkew(XMLExtendedStreamWriter writer, ModelNode allowedClockSkew) throws XMLStreamException {
|
||||
if (!allowedClockSkew.isDefined()) {
|
||||
return;
|
||||
}
|
||||
writer.writeStartElement(Constants.XML.ALLOWED_CLOCK_SKEW);
|
||||
AllowedClockSkew.ALLOWED_CLOCK_SKEW_UNIT.getAttributeMarshaller().marshallAsAttribute(AllowedClockSkew.ALLOWED_CLOCK_SKEW_UNIT, allowedClockSkew, false, writer);
|
||||
ModelNode allowedClockSkewValue = allowedClockSkew.get(Constants.Model.ALLOWED_CLOCK_SKEW_VALUE);
|
||||
char[] chars = allowedClockSkewValue.asString().toCharArray();
|
||||
writer.writeCharacters(chars, 0, chars.length);
|
||||
writer.writeEndElement();
|
||||
}
|
||||
|
||||
void writeKeyStore(XMLExtendedStreamWriter writer, ModelNode model) throws XMLStreamException {
|
||||
if (!model.isDefined()) {
|
||||
return;
|
||||
|
|
|
@ -85,3 +85,6 @@ keycloak-saml.IDP.SingleLogoutService.responseBinding=HTTP method to use for res
|
|||
keycloak-saml.IDP.SingleLogoutService.postBindingUrl=Endpoint URL for posting
|
||||
keycloak-saml.IDP.SingleLogoutService.redirectBindingUrl=Endpoint URL for redirects
|
||||
keycloak-saml.IDP.Key=Key definition for identity provider
|
||||
keycloak-saml.IDP.AllowedClockSkew=Allowed clock skew between the IDP and the SP
|
||||
keycloak-saml.IDP.AllowedClockSkew.value=Allowed clock skew value between the IDP and the SP
|
||||
keycloak-saml.IDP.AllowedClockSkew.unit=Time unit for the value of the clock skew. Values: MINUTES, SECONDS, MILLISECONDS, MICROSECONDS, NANOSECONDS
|
||||
|
|
|
@ -105,6 +105,11 @@
|
|||
<xs:element name="SingleSignOnService" minOccurs="1" maxOccurs="1" type="single-signon-type"/>
|
||||
<xs:element name="SingleLogoutService" minOccurs="0" maxOccurs="1" type="single-logout-type"/>
|
||||
<xs:element name="Keys" minOccurs="0" maxOccurs="1" type="keys-type"/>
|
||||
<xs:element name="AllowedClockSkew" type="allowed-clock-skew-type" minOccurs="0" maxOccurs="1">
|
||||
<xs:annotation>
|
||||
<xs:documentation>This defines the allowed clock skew between IDP and SP in milliseconds. The default value is 0.</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:element>
|
||||
</xs:all>
|
||||
<xs:attribute name="entityID" type="xs:string" use="required">
|
||||
<xs:annotation>
|
||||
|
@ -336,4 +341,28 @@
|
|||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="allowed-clock-skew-type">
|
||||
<xs:annotation>
|
||||
<xs:documentation>The value is the allowed clock skew between the IDP and the SP.</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:simpleContent>
|
||||
<xs:extension base="xs:positiveInteger">
|
||||
<xs:attribute name="unit" type="clock-skew-unit-type"/>
|
||||
</xs:extension>
|
||||
</xs:simpleContent>
|
||||
</xs:complexType>
|
||||
<xs:simpleType name="clock-skew-unit-type">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Time unit for the value of the clock skew.</xs:documentation>
|
||||
</xs:annotation>
|
||||
<xs:restriction base="xs:string">
|
||||
<xs:enumeration value="MINUTES" />
|
||||
<xs:enumeration value="SECONDS" />
|
||||
<xs:enumeration value="MILLISECONDS" />
|
||||
<xs:enumeration value="MICROSECONDS" />
|
||||
<xs:enumeration value="NANOSECONDS" />
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
</xs:schema>
|
||||
|
|
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
* Copyright 2019 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* 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 org.keycloak.subsystem.adapter.saml.extension;
|
||||
|
||||
import org.jboss.as.subsystem.test.AbstractSubsystemBaseTest;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Properties;
|
||||
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.controller.ExpressionResolver;
|
||||
import org.jboss.as.controller.PathAddress;
|
||||
import org.jboss.as.controller.PathElement;
|
||||
import org.jboss.as.controller.descriptions.ModelDescriptionConstants;
|
||||
import org.jboss.as.model.test.ModelTestUtils;
|
||||
import org.jboss.as.subsystem.test.KernelServices;
|
||||
import org.jboss.dmr.ModelNode;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
import org.w3c.dom.DOMException;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Test case for AllowedClockSkew subsystem configuration.
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class SubsystemParsingAllowedClockSkewTestCase extends AbstractSubsystemBaseTest {
|
||||
|
||||
private String subsystemXml = null;
|
||||
|
||||
@Rule
|
||||
public final ExpectedException exception = ExpectedException.none();
|
||||
|
||||
public SubsystemParsingAllowedClockSkewTestCase() {
|
||||
super(KeycloakSamlExtension.SUBSYSTEM_NAME, new KeycloakSamlExtension());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getSubsystemXml() throws IOException {
|
||||
return subsystemXml;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getSubsystemXsdPath() throws Exception {
|
||||
return "schema/wildfly-keycloak-saml_1_2.xsd";
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] getSubsystemTemplatePaths() throws IOException {
|
||||
return new String[]{
|
||||
"/subsystem-templates/keycloak-saml-adapter.xml"
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Properties getResolvedProperties() {
|
||||
return System.getProperties();
|
||||
}
|
||||
|
||||
private void setSubsystemXml(String value, String unit) throws IOException {
|
||||
try {
|
||||
String template = readResource("keycloak-saml-1.2.xml");
|
||||
if (value != null) {
|
||||
// assign the AllowedClockSkew element using DOM
|
||||
DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
|
||||
Document doc = db.parse(new InputSource(new StringReader(template)));
|
||||
// create the skew element
|
||||
Element allowedClockSkew = doc.createElement(Constants.XML.ALLOWED_CLOCK_SKEW);
|
||||
if (unit != null) {
|
||||
allowedClockSkew.setAttribute(Constants.XML.ALLOWED_CLOCK_SKEW_UNIT, unit);
|
||||
}
|
||||
allowedClockSkew.setTextContent(value);
|
||||
// locate the IDP and insert the node
|
||||
XPath xPath = XPathFactory.newInstance().newXPath();
|
||||
NodeList nodeList = (NodeList) xPath.compile("/subsystem/secure-deployment[1]/SP/IDP").evaluate(doc, XPathConstants.NODESET);
|
||||
nodeList.item(0).appendChild(allowedClockSkew);
|
||||
// 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(doc), new StreamResult(writer));
|
||||
subsystemXml = writer.getBuffer().toString();
|
||||
} else {
|
||||
subsystemXml = template;
|
||||
}
|
||||
} catch (DOMException | ParserConfigurationException | SAXException | TransformerException | XPathExpressionException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private PathAddress getIdpPath() {
|
||||
return PathAddress.EMPTY_ADDRESS
|
||||
.append(PathElement.pathElement(ModelDescriptionConstants.SUBSYSTEM, KeycloakSamlExtension.SUBSYSTEM_NAME))
|
||||
.append(PathElement.pathElement(Constants.Model.SECURE_DEPLOYMENT, "my-app.war"))
|
||||
.append(PathElement.pathElement(Constants.Model.SERVICE_PROVIDER, "http://localhost:8080/sales-post-enc/"))
|
||||
.append(PathElement.pathElement(Constants.Model.IDENTITY_PROVIDER, "idp"));
|
||||
}
|
||||
|
||||
private void testSubsystem(String value, String unit, int realValue, String realUnit) throws Exception {
|
||||
setSubsystemXml(value, unit);
|
||||
// perform the common test
|
||||
KernelServices s = super.standardSubsystemTest(null, true);
|
||||
// get the values for the AllowedClockSkew parameters
|
||||
ModelNode idp = ModelTestUtils.getSubModel(s.readWholeModel(), getIdpPath());
|
||||
ModelNode allowedClockSkew = idp.get(Constants.Model.ALLOWED_CLOCK_SKEW);
|
||||
if (value != null) {
|
||||
Assert.assertTrue(allowedClockSkew.isDefined());
|
||||
ModelNode allowedClockSkewValue = allowedClockSkew.get(Constants.Model.ALLOWED_CLOCK_SKEW_VALUE);
|
||||
ModelNode allowedClockSkewUnit = allowedClockSkew.get(Constants.Model.ALLOWED_CLOCK_SKEW_UNIT);
|
||||
allowedClockSkewValue = ExpressionResolver.TEST_RESOLVER.resolveExpressions(allowedClockSkewValue);
|
||||
allowedClockSkewUnit = ExpressionResolver.TEST_RESOLVER.resolveExpressions(allowedClockSkewUnit);
|
||||
Assert.assertEquals(realValue, allowedClockSkewValue.asInt());
|
||||
if (unit != null) {
|
||||
Assert.assertEquals(realUnit, allowedClockSkewUnit.asString());
|
||||
} else {
|
||||
Assert.assertFalse(allowedClockSkewUnit.isDefined());
|
||||
}
|
||||
} else {
|
||||
Assert.assertFalse(allowedClockSkew.isDefined());
|
||||
}
|
||||
}
|
||||
|
||||
private void testSubsystem(String value, String unit) throws Exception {
|
||||
testSubsystem(value, unit, value == null? -1 : Integer.parseInt(value.trim()), unit);
|
||||
}
|
||||
|
||||
private void testSchema(String value, String unit) throws Exception {
|
||||
setSubsystemXml(value, unit);
|
||||
super.testSchema();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Override
|
||||
public void testSubsystem() throws Exception {
|
||||
testSubsystem(null, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Override
|
||||
public void testSchema() throws Exception {
|
||||
testSchema(null, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubsystemAllowedClockSkewWithUnit() throws Exception {
|
||||
testSubsystem("3500", "MILLISECONDS");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSchemaAllowedClockSkewWithUnit() throws Exception {
|
||||
testSchema("3500", "MILLISECONDS");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubsystemAllowedClockSkewWithoutUnit() throws Exception {
|
||||
testSubsystem("1", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSchemaAllowedClockSkewWithoutUnit() throws Exception {
|
||||
testSchema("1", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSubsystemAllowedClockSkewWithSpaces() throws Exception {
|
||||
testSubsystem("\n 20 \n ", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorOnNonInteger() throws Exception {
|
||||
exception.expect(XMLStreamException.class);
|
||||
exception.expectMessage("WFLYCTL0097");
|
||||
testSubsystem("invalid-value", null, -1, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorOnNonPositiveInteger() throws Exception {
|
||||
exception.expect(XMLStreamException.class);
|
||||
exception.expectMessage("WFLYCTL0117");
|
||||
testSubsystem("0", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testErrorNoValidUnit() throws Exception {
|
||||
exception.expect(XMLStreamException.class);
|
||||
exception.expectMessage("WFLYCTL0248");
|
||||
testSubsystem("30", "invalid-unit");
|
||||
}
|
||||
|
||||
// For the moment no expressions allowed as the rest of the subsystem doesn't resolve expressions
|
||||
//@Test
|
||||
//public void testExpression() throws Exception {
|
||||
// System.setProperty("test.prop.SKEW_TIME", "30");
|
||||
// System.setProperty("test.prop.SKEW_UNIT", "MILLISECONDS");
|
||||
// try {
|
||||
// testSubsystem("${test.prop.SKEW_TIME}", "${test.prop.SKEW_UNIT}", 30, "MILLISECONDS");
|
||||
// } finally {
|
||||
// System.clearProperty("test.prop.SKEW_TIME");
|
||||
// System.clearProperty("test.prop.SKEW_UNIT");
|
||||
// }
|
||||
//}
|
||||
}
|
Loading…
Reference in a new issue