Java tutorial
/* * * Copyright 2016 Simon Gfeller * * 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 ch.simuonline.idh.attribute.resolver.dc.aq; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.annotation.Nonnull; import javax.net.ssl.KeyManager; import javax.net.ssl.TrustManager; import org.joda.time.DateTime; import org.joda.time.chrono.ISOChronology; import org.opensaml.core.xml.XMLObject; import org.opensaml.core.xml.XMLObjectBuilderFactory; import org.opensaml.core.xml.config.XMLObjectProviderRegistrySupport; import org.opensaml.core.xml.io.MarshallingException; import org.opensaml.core.xml.schema.XSString; import org.opensaml.core.xml.util.XMLObjectSupport; import org.opensaml.messaging.context.InOutOperationContext; import org.opensaml.messaging.context.MessageContext; import org.opensaml.saml.common.SAMLVersion; import org.opensaml.saml.common.assertion.ValidationContext; import org.opensaml.saml.common.assertion.ValidationResult; import org.opensaml.saml.saml2.assertion.SAML20AssertionValidator; import org.opensaml.saml.saml2.assertion.SAML2AssertionValidationParameters; import org.opensaml.saml.saml2.core.Assertion; import org.opensaml.saml.saml2.core.Attribute; import org.opensaml.saml.saml2.core.AttributeQuery; import org.opensaml.saml.saml2.core.AttributeStatement; import org.opensaml.saml.saml2.core.Conditions; import org.opensaml.saml.saml2.core.Issuer; import org.opensaml.saml.saml2.core.Response; import org.opensaml.saml.saml2.core.Subject; import org.opensaml.saml.security.impl.SAMLSignatureProfileValidator; import org.opensaml.security.x509.X509Credential; import org.opensaml.security.x509.X509Support; import org.opensaml.security.x509.impl.X509KeyManagerX509CredentialAdapter; import org.opensaml.soap.client.http.HttpSOAPClient; import org.opensaml.soap.messaging.context.SOAP11Context; import org.opensaml.soap.soap11.Body; import org.opensaml.soap.soap11.Envelope; import org.opensaml.xmlsec.impl.StaticSignatureValidationParametersResolver; import org.opensaml.xmlsec.signature.Signature; import org.opensaml.xmlsec.signature.support.SignatureException; import org.opensaml.xmlsec.signature.support.SignatureValidator; import org.opensaml.xmlsec.signature.support.impl.BaseSignatureTrustEngine; import org.opensaml.xmlsec.signature.support.impl.ExplicitKeySignatureTrustEngine; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Element; import ch.simuonline.idh.webflow.AttributeQueryContext; import net.shibboleth.idp.attribute.IdPAttribute; import net.shibboleth.idp.attribute.IdPAttributeValue; import net.shibboleth.idp.attribute.StringAttributeValue; import net.shibboleth.idp.attribute.resolver.AbstractDataConnector; import net.shibboleth.idp.attribute.resolver.ResolutionException; import net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext; import net.shibboleth.idp.attribute.resolver.context.AttributeResolverWorkContext; import net.shibboleth.idp.relyingparty.impl.SigningCredentialsResolver; import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit; import net.shibboleth.utilities.java.support.component.ComponentInitializationException; import net.shibboleth.utilities.java.support.component.ComponentSupport; import net.shibboleth.utilities.java.support.httpclient.HttpClientBuilder; import net.shibboleth.utilities.java.support.httpclient.TLSSocketFactoryBuilder; import net.shibboleth.utilities.java.support.logic.Constraint; import net.shibboleth.utilities.java.support.primitive.StringSupport; import net.shibboleth.utilities.java.support.resolver.CriteriaSet; import net.shibboleth.utilities.java.support.xml.BasicParserPool; import net.shibboleth.utilities.java.support.xml.SerializeSupport; /** * Data Connector which resolves attributes with an SAML2 attribute query to an attribute authority * Visit {@link https://github.com/gfels4/identity_hub} for more informations. * * @author Simon Gfeller * */ public class AttributeQueryDataConnector extends AbstractDataConnector { /** Class logger. */ @Nonnull private final Logger log = LoggerFactory.getLogger(AttributeQueryDataConnector.class); /** Strategy to resolve target informations for the query*/ @NonnullAfterInit private TargetDeterminationStrategy targetResolvingStrategy; /** Builder for the attribute Query element */ @NonnullAfterInit private AttributeQueryBuilder attributeQueryBuilder; /** Builder for the attribute Query element */ @NonnullAfterInit private AttributeQueryKeyManager keyManager; /** Determines if a signature is required or not */ private boolean signatureRequired; /** Determines if an AttributeConsumerService with requestedAttributes from the SP Metadatas is required */ private boolean requireMetadataAttributes; public void setRequireMetadataAttributes(boolean requireMetadataAttributes) { ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this); ComponentSupport.ifDestroyedThrowDestroyedComponentException(this); this.requireMetadataAttributes = requireMetadataAttributes; } public void setSignatureRequired(boolean signatureRequired) { ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this); ComponentSupport.ifDestroyedThrowDestroyedComponentException(this); this.signatureRequired = signatureRequired; } /** * Sets the {@link AttributeQueryBuilder} used for building the attribute query element * * @param newAttributeQueryBuilder the new {@link AttributeQueryBuilder} */ public void setAttributeQueryBuilder(@Nonnull final AttributeQueryBuilder newAttributeQueryBuilder) { ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this); ComponentSupport.ifDestroyedThrowDestroyedComponentException(this); this.attributeQueryBuilder = Constraint.isNotNull(newAttributeQueryBuilder, "AttributeQueryBuilder can not be null"); } /** * Sets the {@link AttributeQueryKeyManager} which holds the private key and the certificate for signing the * attribute query and also for the TLS connection * * @param newKeyManager The {@link AttributeQueryKeyManager} holding the certificate and the private key */ public void setAttributeQueryKeyManager(@Nonnull final AttributeQueryKeyManager newKeyManager) { ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this); ComponentSupport.ifDestroyedThrowDestroyedComponentException(this); this.keyManager = Constraint.isNotNull(newKeyManager, "AttributeQueryKeyManager can not be null"); } /** * Sets the {@link TargetResolvingStrategy} used for the attribute query, its used * to get target informations for the AttributeQuery. * * @param newTargetResolvingStrategy The {@link TargetResolvingStrategy} used for the AttributeQuery */ public void setTargetResolvingStrategy(@Nonnull final TargetDeterminationStrategy newTargetResolvingStrategy) { ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this); ComponentSupport.ifDestroyedThrowDestroyedComponentException(this); this.targetResolvingStrategy = Constraint.isNotNull(newTargetResolvingStrategy, "TargetResolvingStrategy can not be null"); } /** {@inheritDoc} */ @Override @Nonnull protected Map<String, IdPAttribute> doDataConnectorResolve( @Nonnull final AttributeResolutionContext resolutionContext, @Nonnull final AttributeResolverWorkContext workContext) throws ResolutionException { ComponentSupport.ifNotInitializedThrowUninitializedComponentException(this); final DateTime start = DateTime.now(); log.debug("{} Begin with resolving Attributes, the principal is: {}, Start time of AQ Extension: {}!", getLogPrefix(), resolutionContext.getPrincipal(), start); // get informations about the query target AQTarget queryTarget = targetResolvingStrategy.resolveTargetInformations(resolutionContext); if (queryTarget == null || StringSupport.trimOrNull(queryTarget.getNameID()) == null || (signatureRequired && queryTarget.getCertificate() == null) || StringSupport.trimOrNull(queryTarget.getEntityID()) == null || StringSupport.trimOrNull(queryTarget.getAttributeQueryURL()) == null) { log.warn("{} No working target informations for the attribute query found for principal: {}", getLogPrefix(), resolutionContext.getPrincipal()); return null; } // get the aq context and check if there are attributes to resolve from the metadata of this service provider AttributeQueryContext aqcontext = resolutionContext.getSubcontext(AttributeQueryContext.class, false); List<AQAttribute> attributesToResolve = null; if (aqcontext != null) { attributesToResolve = aqcontext.getAttributesToResolve(); } // if there are no attributes to resolve from the metadatas, no attribtue query is needed if ((attributesToResolve == null || attributesToResolve.isEmpty()) && requireMetadataAttributes) { log.warn("{} Attributes from the SP Metadata required, but no attributes found", getLogPrefix()); return null; } // Building the Attribute Query AttributeQuery query = attributeQueryBuilder.buildAttributeQuery(queryTarget, keyManager, attributesToResolve); if (query == null) { log.warn("{} Attribute query build failed", getLogPrefix()); return null; } // Method for creating the SOAP Envelope Envelope envelope = createSOAPEnvelope(query); try { Element dom = XMLObjectSupport.marshall(envelope); log.debug("{} Print attribute query with envelope {}", getLogPrefix(), SerializeSupport.prettyPrintXML(dom)); } catch (MarshallingException e) { log.debug("{} Unable to print out created attribute query", getLogPrefix()); } // Create InOutOperationContext to send the soap envelope InOutOperationContext<Envelope, Envelope> ioCtx = new InOutOperationContext<Envelope, Envelope>(null, null); ioCtx.setOutboundMessageContext(new MessageContext<Envelope>()); ioCtx.getOutboundMessageContext().getSubcontext(SOAP11Context.class, true).setEnvelope(envelope); // Get a SOAP CLient HttpSOAPClient soapClient = getSOAPClient(); // Send the AttributeQuery try { soapClient.send(queryTarget.getAttributeQueryURL(), ioCtx); } catch (Exception e) { log.warn("{} Sending SOAP Message failed {}", getLogPrefix(), e); return null; } // Get the Response Envelope Envelope SOAPResponse = ioCtx.getInboundMessageContext().getSubcontext(SOAP11Context.class, false) .getEnvelope(); if (SOAPResponse == null || SOAPResponse.getBody() == null) { log.warn("{} No SOAP Response Recieved", getLogPrefix()); return null; } Response samlResponse = null; final List<XMLObject> bodyElements = SOAPResponse.getBody().getUnknownXMLObjects(); for (XMLObject element : bodyElements) { if (element instanceof Response) { samlResponse = (Response) element; } } if (samlResponse == null) { log.warn("{} SOAP Response Body containts no SAML Response!", getLogPrefix()); return null; } // Log the Response try { Element dom = XMLObjectSupport.marshall(samlResponse); log.debug("{} Print SAML Response: {}", getLogPrefix(), SerializeSupport.prettyPrintXML(dom)); } catch (MarshallingException e) { log.warn("{} Unable to print SAML Response. {}", getLogPrefix(), e); } // Validate the saml response if (!validateResponse(samlResponse, query.getID(), query.getIssueInstant(), queryTarget)) { log.warn("{} SAML Response from the AttributeQuery is not valid!", getLogPrefix()); return null; } // Get the attribute statement final List<AttributeStatement> attributeStatements = samlResponse.getAssertions().get(0) .getAttributeStatements(); // if there is no attribue statement, the method returns null (no attributes) if (attributeStatements == null || attributeStatements.size() == 0) return null; // Create the Attributes to release final List<Attribute> responseAttributes = attributeStatements.get(0).getAttributes(); final Map<String, IdPAttribute> returnMap = new HashMap<>(responseAttributes.size()); for (Attribute responseAttribute : responseAttributes) { final IdPAttribute idpAttribute = new IdPAttribute(responseAttribute.getFriendlyName()); final List<XMLObject> responseAttributeValues = responseAttribute.getAttributeValues(); final ArrayList<IdPAttributeValue<?>> idpAttributeValues = new ArrayList<>( responseAttributeValues.size()); for (XMLObject responseAttributeValue : responseAttributeValues) { if (responseAttributeValue instanceof XSString) { idpAttributeValues .add(StringAttributeValue.valueOf(((XSString) responseAttributeValue).getValue())); } } log.debug("{} New Attribute produced from AttributeStatement, friendly name: {}, name: {}", getLogPrefix(), responseAttribute.getFriendlyName(), responseAttribute.getName()); idpAttribute.setValues(idpAttributeValues); returnMap.put(idpAttribute.getId(), idpAttribute); } log.debug("{} Duration of AQ Data Connector: {}ms", getLogPrefix(), (DateTime.now().getMillis() - start.getMillis())); return returnMap; } /** * * Validator of the SAML Response * * @param response the response object to validate * @param randomID the randomID from the attribute query * @param queryTime the timestamp from the attribute query * @param nameID the nameID from the query subject * @param entityID the entityID from the attribute authority * @return true if validation of the response was positive, false if something with the response is wrong * @throws ResolutionException throws if some element of the response is not readable */ protected boolean validateResponse(@Nonnull Response response, String randomID, DateTime queryTime, AQTarget queryTarget) throws ResolutionException { try { // Check SAML version if (response.getVersion() != SAMLVersion.VERSION_20) { log.warn("{} Validation of SALM2 response failed! Unsupported SAML version of response: {} ", getLogPrefix(), response.getVersion()); return false; } // It should have exactly one assertion in this response if (response.getAssertions().size() != 1) { log.warn( "{} Validation of SALM2 response failed! There are {} assertions, but it must have exactly 1 assertion!", getLogPrefix(), response.getAssertions().size()); return false; } // Validate if response status is success if (!response.getStatus().getStatusCode().getValue() .equals("urn:oasis:names:tc:SAML:2.0:status:Success")) { log.warn( "{} Validation of SALM2 response failed! Wrong Status Code: {}, it has to be urn:oasis:names:tc:SAML:2.0:status:Success", getLogPrefix(), response.getStatus().getStatusCode().getValue()); return false; } Assertion assertion = response.getAssertions().get(0); if (assertion.getVersion() != SAMLVersion.VERSION_20) { log.warn("{} Validation of SALM2 response failed! Assertion is not a SAML 2.0 version Assertion", getLogPrefix()); return false; } // Check if issuer has the right entityID Issuer assertionIssuer = assertion.getIssuer(); String entityID = queryTarget.getEntityID(); if (!assertionIssuer.getValue().equals(entityID)) { log.warn("{} Validation of SALM2 response failed! Wrong Issuer {}, it should be {}", getLogPrefix(), assertion.getIssuer().getValue(), entityID); return false; } // verify signature of assertion if (assertion.isSigned() && signatureRequired) { log.debug("{} Begin with checking signature", getLogPrefix()); Signature signature = assertion.getSignature(); // checks if its a valid signature profile SAMLSignatureProfileValidator spv = new SAMLSignatureProfileValidator(); try { spv.validate(signature); log.debug("{} Signature hat correct profile", getLogPrefix()); } catch (SignatureException e) { log.warn("{} Validation of SALM2 response failed! Exception while validating the exception: {}", getLogPrefix(), e); return false; } // String certificate = "MIIDLDCCAhSgAwIBAgIVAPhMcSU5PCjjIosHSOpreF2ztyCZMA0GCSqGSIb3DQEBBQUAMBsxGTAXBgNVBAMMEHNoaWJhYS50aS5iZmguY2gwHhcNMTUwNDIxMDYzNTM2WhcNMzUwNDIxMDYzNTM2WjAbMRkwFwYDVQQDDBBzaGliYWEudGkuYmZoLmNoMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkmZJpYDL6CbQtzUBrwvfPOAdihQyI+BZiio6060GC4H8ObyaBIbRNFVkaG6FVlfBR8XVvB6SiRIMjd4pmWii0/lfhy67/61dHs35+AwSKzYcxThsov8YtTnyhj3bAij0Ved2cmdx3ZCQyfCmPH2mI6bFssDxMczRT4oTtCCmmOQH3XoebT2HJ8CMXqTxBQzQm0f5voc8BfeGVBjduDGS5D14kS7couuTmgTIa91EnypeXH67ZHk8QlRcPFRyGHn2s3ivPjbwRrvWtqpHCry14MvQVtOcu8TJ0OsN4+h3ds2OX+ZigIcxVOfBx+VM9O8vlHwzrCm0ACoi9uy0mVbDZQIDAQABo2cwZTAdBgNVHQ4EFgQUdWPQ51FiJv3dS+MEmDlF8XcSWzkwRAYDVR0RBD0wO4IQc2hpYmFhLnRpLmJmaC5jaIYnaHR0cHM6Ly9zaGliYWEudGkuYmZoLmNoL2lkcC9zaGliYm9sZXRoMA0GCSqGSIb3DQEBBQUAA4IBAQAxXuCR2Xd7cM+LTRkateGSu3SblomsOFnWJT3hizLsRa9y2kbQz14NAn3KF5pQ1srEX4AS18AM6YVjkR2r8if96m8PmrGxwGUNwK6AYXoBQ/oRq3ZC1DZJFS8qmgmX9wr96Gb0yJbFmJeHOfvqgzPSdB+oZUX3RTPXF6QOnt7+LFvSf1EfDwadp8lq8MQAqtHszHYFMkRGPJC+KBEC6PNkFa36K3pD+E2yh9Q51Yg5eic7GyG5qyZeYIuo3hERS9w3ZlLGjQ+mkvxN5gM3U7fJAYkcdc+crABVy/XAuLoLUMiIUD0gaKjs2enRB+LQVX+rjyiyukETAvdftadGFxv8"; // X509Certificate cert = X509Support.decodeCertificate(certificate); X509Certificate cert = queryTarget.getCertificate(); AttributeQueryKeyManager keyManager = new AttributeQueryKeyManager(cert); X509Credential validationCredential = new X509KeyManagerX509CredentialAdapter(keyManager, "verification"); try { SignatureValidator.validate(signature, validationCredential); log.debug("{} Signature validated, no problem", getLogPrefix()); } catch (SignatureException e) { log.warn("{} Validation of SALM2 response failed! Assertion Signature is not correct!", getLogPrefix()); return false; } } else if (!assertion.isSigned() && signatureRequired) { log.warn( "{} Validation of SALM2 response failed! Assertion Signature is required but this assertion is not sigened!", getLogPrefix()); return false; } else { log.warn("{} No signature check required!", getLogPrefix()); } // CONDITIONS OF ASSERTION Conditions conditions = assertion.getConditions(); if (conditions != null) { log.debug("{} Check conditions of assertion!", getLogPrefix()); DateTime now = DateTime.now(); DateTime notBefore = conditions.getNotBefore(); log.debug("Evaluating Conditions NotBefore '{}' against now(+5min) '{}'", notBefore, now.plusMinutes(5)); if (notBefore != null && notBefore.isAfter(now.plusMinutes(5))) { log.warn("{} Validation of SALM2 response failed! The condition not before {} failed.", getLogPrefix(), notBefore); return false; } DateTime notOnOrAfter = conditions.getNotOnOrAfter(); log.debug("Evaluating Conditions NotOnOrAfter '{}' against now(-5min) '{}'", notOnOrAfter, now.minusMinutes(5)); if (notOnOrAfter != null && notOnOrAfter.isBefore(now.minusMinutes(5))) { log.warn("{} Validation of SALM2 response failed! The condition not on or after {} failed.", getLogPrefix(), notOnOrAfter); return false; } } else { log.debug("{} Asertion does not contain conditions!", getLogPrefix()); } // SUBJECT OF ASSERTION Subject assertionSubject = assertion.getSubject(); if (assertionSubject == null) { log.warn("{} Validation of SALM2 response failed! No Subject found", getLogPrefix()); return false; } // check if the nameid is correct String nameID = queryTarget.getNameID(); if (!assertionSubject.getNameID().getValue().equals(nameID)) { log.warn("{} Validation of SALM2 response failed! Wrong nameID: {}, expected: {}", getLogPrefix(), assertion.getSubject().getNameID().getValue(), nameID); return false; } // check if the in response to id is correct if (!assertionSubject.getSubjectConfirmations().get(0).getSubjectConfirmationData().getInResponseTo() .equals(randomID)) { log.warn("{} Validation of SALM2 response failed! Wrong InResponseTo ID: {}, it has to be {}", getLogPrefix(), response.getInResponseTo(), randomID); return false; } // verify that the assertion has not more than one attribue statements if (assertion.getAttributeStatements().size() > 1) { log.warn( "{} Validation of SALM2 response failed! {} attribute statements, but expected is 0 or one {}", getLogPrefix(), assertion.getAttributeStatements().size()); return false; } } catch (Exception e) { log.warn( "{} Validation of SALM2 response failed! {} Can not read all Elements of the assertion to verify response! Exception: {}", getLogPrefix(), e); return false; } log.debug("{} SAML2 Response validated, it's valid", getLogPrefix()); return true; } /** * Builds the HTTPSOAPCLient to send the attribute query, with client TLS * * @return the initialized HTTPSOAPClient object ready to send a soap message * @throws ResolutionException throws if there is a problem while creating the HTTPSOAPClient */ @Nonnull protected HttpSOAPClient getSOAPClient() throws ResolutionException { // Client TLS support ArrayList<KeyManager> keyManagerList = new ArrayList<>(1); keyManagerList.add(keyManager); ArrayList<TrustManager> trustManagerList = new ArrayList<>(1); trustManagerList.add(new DelegateToApplicationX509TrustManager()); TLSSocketFactoryBuilder tlsFactoryBuilder = new TLSSocketFactoryBuilder(); tlsFactoryBuilder.setKeyManagers(keyManagerList); tlsFactoryBuilder.setTrustManagers(trustManagerList); // generate http clientbuilder HttpClientBuilder clientBuilder = new HttpClientBuilder(); clientBuilder.setTLSSocketFactory(tlsFactoryBuilder.build()); // timeout after 5 seconds if no connection possible //clientBuilder.setConnectionTimeout(5000); // generate parserpool BasicParserPool parserPool = new BasicParserPool(); parserPool.setNamespaceAware(true); try { parserPool.initialize(); } catch (Exception e) { throw new ResolutionException(getLogPrefix() + "Inizialize Parser pool failed", e); } // Build client and send the soap message HttpSOAPClient soapClient = new HttpSOAPClient(); try { soapClient.setHttpClient(clientBuilder.buildClient()); soapClient.setParserPool(parserPool); soapClient.initialize(); } catch (Exception e) { throw new ResolutionException(getLogPrefix() + "SOAP Client Init failed!!!", e); } return soapClient; } /** * Method to create a SOAP envelope and add the query to the body of the envelope * * @param query the {@link AttributeQuery} Element for the envelope body * @return the envelope with the attribute query in its body element */ @Nonnull protected Envelope createSOAPEnvelope(@Nonnull AttributeQuery query) { final XMLObjectBuilderFactory bf = XMLObjectProviderRegistrySupport.getBuilderFactory(); Envelope envelope = bf.<Envelope>getBuilderOrThrow(Envelope.TYPE_NAME).buildObject( Envelope.DEFAULT_ELEMENT_NAME.getNamespaceURI(), Envelope.DEFAULT_ELEMENT_NAME.getLocalPart(), Envelope.DEFAULT_ELEMENT_NAME.getPrefix()); Body body = bf.<Body>getBuilderOrThrow(Body.TYPE_NAME).buildObject( Body.DEFAULT_ELEMENT_NAME.getNamespaceURI(), Body.DEFAULT_ELEMENT_NAME.getLocalPart(), Body.DEFAULT_ELEMENT_NAME.getPrefix()); body.getUnknownXMLObjects().add(query); envelope.setBody(body); return envelope; } @Override protected void doInitialize() throws ComponentInitializationException { super.doInitialize(); if (targetResolvingStrategy == null) { throw new ComponentInitializationException(getLogPrefix() + " No targetResolvingStrategy for the AttribtueQueryDataConnector configured, please adjust the DataConnector XML Settings"); } if (attributeQueryBuilder == null) { throw new ComponentInitializationException(getLogPrefix() + " No attributeQueryBuilder for the AttribtueQueryDataConnector configured, please adjust the DataConnector XML Settings"); } if (keyManager == null) { throw new ComponentInitializationException(getLogPrefix() + " No keyManager AttribtueQueryDataConnector configured, please adjust the DataConnector XML Settings"); } } }