/** * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2005 Sun Microsystems Inc. All Rights Reserved * * The contents of this file are subject to the terms * of the Common Development and Distribution License * (the License). You may not use this file except in * compliance with the License. * * You can obtain a copy of the License at * https://opensso.dev.java.net/public/CDDLv1.0.html or * opensso/legal/CDDLv1.0.txt * See the License for the specific language governing * permission and limitations under the License. * * When distributing Covered Code, include this CDDL * Header Notice in each file and include the License file * at opensso/legal/CDDLv1.0.txt. * If applicable, add the following below the CDDL Header, * with the fields enclosed by brackets [] replaced by * your own identifying information: * "Portions Copyrighted [year] [name of copyright owner]" * * $Id: ServiceSchemaImpl.java,v 1.7 2008/06/25 05:44:05 qcheng Exp $ * * Portions Copyrighted 2012-2016 ForgeRock AS. */ package com.sun.identity.sm; import com.iplanet.am.util.SystemProperties; import com.sun.identity.common.CaseInsensitiveHashMap; import com.sun.identity.common.CaseInsensitiveHashSet; import com.sun.identity.shared.Constants; import com.sun.identity.shared.debug.Debug; import com.sun.identity.shared.xml.XMLUtils; import com.iplanet.sso.SSOToken; import com.iplanet.ums.IUMSConstants; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.TreeSet; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * The class ServiceSchema provides interfaces to manage the * schema information of a service. The schema for a service can be one of the * following types: GLOBAL, ORGANIZATION, DYNAMIC, USER, and POLICY. */ class ServiceSchemaImpl { // ServiceSchema XML node Node schemaNode; // Instance variables String name; String i18nKey; String i18nFileName; String statusAttribute; String validate; String inheritance; String resourceName; String hideConfigUI; String realmCloneable; // Attribute & sub-schema variables Set serviceAttributes; Set searchableAttributeNames; Map attrSchemas; Map attrValidators; Map attrDefaults; Map> attributeExamples; Map attrReadOnlyDefaults; Map subSchemas; ServiceSchemaImpl orgAttrSchema; ServiceSchemaManagerImpl ssm; boolean isOrgAttrSchema; String serviceName; private volatile boolean valid = false; // Debug class static Debug debug = SMSEntry.debug; ServiceSchemaImpl() { // do nothing } // Protected Constructor protected ServiceSchemaImpl(Node node) { this(null, node); } // Protected constructor protected ServiceSchemaImpl(ServiceSchemaManagerImpl ss, Node node) { ssm = ss; update(node); } /** * Returns the name of the schema. */ String getName() { return (name); } /** * Returns the name of the schema for CREST representation. */ String getResourceName() { return resourceName; } /** * Should this service schema be hidden in the Configuration UI. * @return True if it should be hidden. */ boolean isHiddenInConfigUI() { return "yes".equalsIgnoreCase(hideConfigUI); } /** * During the creation of a new organisation/realm the services assigned to the parent realm are copied to the * child realm. This will include the sub configs for that service, which in some cases are realm specific and will * fail validation if copied. The schemas of these sub configs should set {@code realmCloneable} to {@code no} to * avoid being copied. * * @return {@code true} if the config is cloneable between realms. */ boolean isRealmCloneable() { // value is null in some schemas and we want to default to true, so check for "no" rather than "yes" return !"no".equalsIgnoreCase(realmCloneable); } /** * Returns the I18N key that points to the description of the service. */ String getI18NKey() { return ((i18nKey == null) && (ssm != null)) ? ssm.getI18NKey() : i18nKey; } /** * Returns statusAttribute name * */ String getStatusAttribute() { return (statusAttribute); } /** * Returns the value of validate attribute for this schema/subschema */ String getValidate() { return (validate); } /** * Returns true if service schema supports multiple * configurations; false otherwise. */ boolean supportsMultipleConfigurations() { return (inheritance != null) && inheritance.equalsIgnoreCase("multiple"); } /** * Returns the names of the schema attributes defined for the service. It * does not return the schema attributes defined for the sub-schema. */ Set getAttributeSchemaNames() { return new HashSet(attrSchemas.keySet()); } /** * Returns the names of the schema attributes defined for the service which * are searchable. It does not return the schema attributes defined for the * sub-schema. */ protected Set getSearchableAttributeNames() { Set tmpSet = new HashSet(); if (searchableAttributeNames != null && !searchableAttributeNames.isEmpty()) { Iterator itr = searchableAttributeNames.iterator(); while (itr.hasNext()) { String str = (String) itr.next(); if ((isOrgAttrSchema) && (!str.toLowerCase().startsWith( serviceName.toLowerCase()))) { tmpSet.add((serviceName + "-" + str).toLowerCase()); } else { tmpSet.add(str.toLowerCase()); } } } searchableAttributeNames = tmpSet; return (searchableAttributeNames); } Set getServiceAttributeNames() { return (new HashSet(serviceAttributes)); } /** * Returns the schema for an attribute given the name of the attribute, * defined for this service. It returns only the attribute schema defined at * the top level for the service and not from the sub-schema. */ AttributeSchemaImpl getAttributeSchema(String attributeName) { return attrSchemas.get(attributeName); } Set getAttributeSchemas() { return new HashSet(attrSchemas.values()); } /** * Get a map of all the attribute and their default values in this schema */ Map> getAttributeDefaults() { return (SMSUtils.copyAttributes(attrDefaults)); } /** * Get a map of all the attribute and their example values in this schema */ Map getAttributeExamples() { return (SMSUtils.copyAttributes(attributeExamples)); } /** * Get a read only map (Unmodifiable map) of all the attribute and their * default values in this schema */ Map getReadOnlyAttributeDefaults() { return attrReadOnlyDefaults; } /** * Returns the names of sub-schemas for the service. */ Set getSubSchemaNames() { return new HashSet<>(subSchemas.keySet()); } /** * Returns ServiceSchema object given the name of the * service's sub-schema. */ ServiceSchemaImpl getSubSchema(String subSchemaName) throws SMSException { return ((ServiceSchemaImpl) subSchemas.get(subSchemaName)); } /** * Returns ServiceSchema for creating Organizations if * present; else null. */ ServiceSchemaImpl getOrgAttrSchema() { return (orgAttrSchema); } /** * Determines whether each attribute in the attribute set is valid. Iterates * though the set checking each element to see if there is a validator that * needs to execute. */ boolean validateAttributes(Map> attributeSet, boolean encodePassword) throws SMSException { return (validateAttributes(attributeSet, encodePassword, null)); } /** * Determines whether each attribute in the attribute set is valid. This * method additionally takes the organization name that would be passed to * the validation. Iterates though the set checking each element to see if * there is a validator that needs to execute. */ boolean validateAttributes(Map> attributeSet, boolean encodePassword, String orgName) throws SMSException { return (validateAttributes(null, attributeSet, encodePassword, orgName)); } /** * Determines whether each attribute in the attribute set is valid. This * method additionally takes the organization name and SSOToken that would * be passed to the validation. Iterates though the set checking each element * to see if there is a validator that needs to execute. */ boolean validateAttributes(SSOToken ssoToken, Map> attributeSet, boolean encodePassword, String orgName) throws SMSException { return validateAttributes(ssoToken, attributeSet, encodePassword, orgName, false); } /** * Determines whether each attribute in the attribute set is valid. This * method additionally takes the organization name and SSOToken that would * be passed to the validation. Iterates though the set checking each element * to see if there is a validator that needs to execute. Empty sets are permitted if the {@code permitEmpty} * parameter is {@code true}. */ boolean validateAttributes(SSOToken ssoToken, Map> attributeSet, boolean encodePassword, String orgName, boolean permitEmpty) throws SMSException { if (validate != null && validate.equalsIgnoreCase("no")) { // Do not validate attributes in this subschema return (true); } // to check for duplicates (case insensitive) Set asNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); // For each attribute, validate its values for (String attrName : attributeSet.keySet()) { if (asNames.contains(attrName)) { Object[] args = {attrName}; throw new SMSException(IUMSConstants.UMS_BUNDLE_NAME, "sms-attributeschema-duplicates", args); } else { asNames.add(attrName); } if (!attrName.equalsIgnoreCase(SMSUtils.COSPRIORITY)) { Set vals = attributeSet.get(attrName); if (!permitEmpty || !vals.isEmpty()) { validateAttrValues(ssoToken, attrName, vals, encodePassword, orgName); } } } return (true); } /** * Validate default values - permits an empty Set of values. * @param attributeSet The attributes to validate. * @return {@code true} if valid. * @throws SMSException If invalid. */ boolean validateDefaults(Map> attributeSet) throws SMSException { return validateAttributes(null, attributeSet, false, null, true); } public String toString() { StringBuilder sb = new StringBuilder(100); if (getName() != null) { sb.append("Schema name: ").append(getName()).append("\n"); } if (getResourceName() != null) { sb.append("Schema resource name: ").append(getResourceName()).append("\n"); } // Attributes if (attrSchemas.size() > 0) { sb.append("Attribute Schemas:\n"); Iterator items = attrSchemas.keySet().iterator(); while (items.hasNext()) { sb.append(attrSchemas.get(items.next()).toString()); } } // Sub-schemas if (subSchemas.size() > 0) { sb.append("Sub-Schemas:\n"); Iterator items = subSchemas.keySet().iterator(); while (items.hasNext()) { sb.append(subSchemas.get(items.next()).toString()); } } return (sb.toString()); } Node getSchemaNode() { return (schemaNode); } // ----------------------------------------------------------- // Protected methods // ----------------------------------------------------------- synchronized void update(Node sNode) { schemaNode = sNode; if (schemaNode == null) { if (debug.warningEnabled()) { debug.warning("ServiceSchemaImpl::update schema node is NULL"); } name = ""; i18nKey = null; i18nFileName = null; statusAttribute = null; serviceAttributes = new HashSet(); searchableAttributeNames = new HashSet(); attrSchemas = attrValidators = attrDefaults = new HashMap(); subSchemas = new CaseInsensitiveHashMap<>(); attrReadOnlyDefaults = Collections.unmodifiableMap(new HashMap()); } // Get the name and i18nKey name = XMLUtils.getNodeAttributeValue(schemaNode, SMSUtils.NAME); i18nKey = XMLUtils.getNodeAttributeValue(schemaNode, SMSUtils.I18N_KEY); i18nFileName = XMLUtils.getNodeAttributeValue(schemaNode, SMSUtils.PROPERTIES_FILENAME); statusAttribute = XMLUtils.getNodeAttributeValue(schemaNode, SMSUtils.STATUS_ATTRIBUTE); inheritance = XMLUtils.getNodeAttributeValue(schemaNode, SMSUtils.INHERITANCE); validate = XMLUtils .getNodeAttributeValue(schemaNode, SMSUtils.VALIDATE); if (schemaNode.getNodeName().equals(SMSUtils.SUB_SCHEMA)) { resourceName = XMLUtils.getNodeAttributeValue(schemaNode, SMSUtils.RESOURCE_NAME); } else if (ssm != null) { resourceName = ssm.getResourceName(); } hideConfigUI = XMLUtils.getNodeAttributeValue(schemaNode, SMSUtils.HIDE_CONFIG_UI); realmCloneable = XMLUtils.getNodeAttributeValue(schemaNode, SMSUtils.REALM_CLONEABLE); // Update sub-schema's, organization schema and attributes Set newServiceAttributes = new HashSet(); Set newSearchableAttributeNames = new HashSet(); Map newAttrSchemas = new HashMap<>(); Map newAttrValidators = new HashMap(); Map newAttrDefaults = new HashMap(); Map> newAttrExamples = new HashMap<>(); Map newSubSchemas = new CaseInsensitiveHashMap<>(); Map tempUnmodifiableDefaults = new HashMap(); NodeList children = schemaNode.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node node = children.item(i); String name = XMLUtils.getNodeAttributeValue(node, SMSUtils.NAME); if (node.getNodeName().equals(SMSUtils.SCHEMA_ATTRIBUTE)) { AttributeSchemaImpl asi = null; if (attrSchemas != null) { asi = attrSchemas.get(name); } if (asi == null) { asi = new AttributeSchemaImpl(node); } else { // Get the instance of attribute schema and update it asi.update(node); } newAttrSchemas.put(name, asi); if (!asi.isStatusAttribute() && !asi.isServiceIdentifier()) { newServiceAttributes.add(name); } if (asi.isSearchable()) { newSearchableAttributeNames.add(name); } newAttrValidators.put(name, new AttributeValidator(asi)); newAttrDefaults.put(name, asi.getDefaultValues()); newAttrExamples.put(name, asi.getExampleValues()); tempUnmodifiableDefaults.put(name, Collections .unmodifiableSet(asi.getDefaultValues())); } else if (node.getNodeName().equals(SMSUtils.SUB_SCHEMA)) { ServiceSchemaImpl ssi = null; if (subSchemas != null) { ssi = (ServiceSchemaImpl) subSchemas.get(name); } if (ssi == null) { newSubSchemas.put(name, new ServiceSchemaImpl(ssm, node)); } else { ssi.update(node); newSubSchemas.put(name, ssi); } } else if (node.getNodeName().equals(SMSUtils.ORG_ATTRIBUTE_SCHEMA)) { orgAttrSchema = new ServiceSchemaImpl(ssm, node); } } serviceAttributes = newServiceAttributes; attrSchemas = newAttrSchemas; searchableAttributeNames = newSearchableAttributeNames; attrValidators = newAttrValidators; attrDefaults = newAttrDefaults; attributeExamples = newAttrExamples; attrReadOnlyDefaults = Collections .unmodifiableMap(tempUnmodifiableDefaults); subSchemas = newSubSchemas; valid = true; } synchronized void clear() { // org attr-schema if (orgAttrSchema != null) { orgAttrSchema.clear(); orgAttrSchema = null; } // Sub-schemas if (subSchemas.size() > 0) { Iterator items = subSchemas.keySet().iterator(); while (items.hasNext()) { ServiceSchemaImpl ssi = (ServiceSchemaImpl)subSchemas.get(items.next()); ssi.clear(); } } //important to clean all reference to ServiceSchemaManagerImpl ssm = null; //finally clear all subschemas subSchemas.clear(); //when clear is called, this object is no longer valid. valid = false; } AttributeValidator getAttributeValidator(String attrName) { AttributeValidator av = (AttributeValidator) attrValidators .get(attrName); if (av == null) { AttributeSchemaImpl as = getAttributeSchema(attrName); if (as == null) { return null; } av = new AttributeValidator(as); attrValidators.put(attrName, av); } return (av); } // Validates the values for the attribute, for the // given organization name void validateAttrValues(SSOToken ssoToken, String attrName, Set values, boolean encodePassword, String orgName) throws SMSException { if (validate != null && validate.equalsIgnoreCase("no")) { // Do not validate attributes in this subschema return; } AttributeValidator av = getAttributeValidator(attrName); if (av == null) { // Invalid attribute name if (debug.messageEnabled()) { debug.message("ServiceSchemaImpl::validateAttrValues " + "Invalid Attribute: " + attrName + " in service: " + ssm.getName() + " sub-schema: " + name); } String[] args = { attrName }; throw (new InvalidAttributeNameException( IUMSConstants.UMS_BUNDLE_NAME, "sms-validation_failed_invalid_name", args)); } // Check if required attributes have values // %%% Due to comms issue, where they define AttributeSchema // %%% without default values, this check should not be done // %%% if orgName is null // OrgName should be null only during loading of schema // and when ServiceSchema.validateAttributes(...) is called // with no orgName param or if orgName == null if (orgName != null) { AttributeSchemaImpl as = getAttributeSchema(attrName); String anyValue = as.getAny(); if (anyValue != null && anyValue.indexOf("required") > -1 && (values == null || values.isEmpty())) { Object args[] = { attrName }; SMSException smse = new SMSException( IUMSConstants.UMS_BUNDLE_NAME, "sms-required-attribute-delete", args); debug.error( "ServiceSchemaImpl.validateAttrValues: " + attrName + " is a required attribute and cannot" + " be deleted", smse); throw smse; } } // If orgName is not null, pass it as an environment map if (orgName != null) { HashMap env = new HashMap(); env.put(Constants.ORGANIZATION_NAME, orgName); if (ssoToken != null) { env.put(Constants.SSO_TOKEN, ssoToken); } String i18nFileName = (ssm != null) ? ssm.getI18NFileName() : null; av.validate(values, i18nFileName, encodePassword, env); } else { String i18nFileName = (ssm != null) ? ssm.getI18NFileName() : null; av.validate(values, i18nFileName, encodePassword); } validatePlugin(ssoToken, attrName, values); } /** * Validates the attribute with the validation plugin if a plugin has been * registered for this attribute in the service schema. * * @param token * Single Sign On token. * @param attrName * the name of the attribute to validate * @param values * the Set of string values to validate * @return true if the values are valid or there is no validation plugin * registered to the attribute; false otherwise * @throws SMSException * error during instantiating the Java class */ boolean validatePlugin(SSOToken token, String attrName, Set values ) throws SMSException { AttributeSchemaImpl as = getAttributeSchema(attrName); if (as == null) { String[] args = { attrName }; throw new InvalidAttributeNameException( IUMSConstants.UMS_BUNDLE_NAME, "sms-validation_failed_invalid_name", args); } String validatorName = as.getValidator(); if (validatorName == null) { // no validator registered for this service attribute return true; } //if we are a pointer to the empty set and required, and we have an example value rather than a default one //use that example value for now. Once submitted that empty set should be a new empty set not equal to the //EMPTY_SET - see ServiceConfig#getAttributes if (values == Collections.EMPTY_SET && !as.getExampleValues().isEmpty()) { values = as.getExampleValues(); } String[] validators = validatorName.split(" "); for (String validator : validators) { AttributeSchemaImpl validatorAttrSchema = getAttributeSchema(validator); if (validatorAttrSchema != null) { boolean isServerMode = SystemProperties.isServerMode(); Set javaClasses = validatorAttrSchema.getDefaultValues(); for (Object jClass : javaClasses) { String javaClass = (String) jClass; try { serverEndAttrValidation(as, attrName, values, javaClass); } catch (SMSException e) { if (!isServerMode) { clientEndAttrValidation(token, as, attrName, values, javaClass); } else { throw e; } } } } } return true; } private void clientEndAttrValidation( SSOToken token, AttributeSchemaImpl as, String attrName, Set values, String javaClass ) throws SMSException { if (!RemoteServiceAttributeValidator.validate( token, javaClass, values) ) { throwInvalidAttributeValuesException( javaClass.equals("com.sun.identity.sm.RequiredValueValidator"), attrName, as); } } private void serverEndAttrValidation( AttributeSchemaImpl as, String attrName, Set values, String javaClass ) throws SMSException { try { Class clazz = Class.forName(javaClass); if (ServiceAttributeValidator.class.isAssignableFrom(clazz)) { ServiceAttributeValidator validator = (ServiceAttributeValidator) clazz.newInstance(); validatePlugin(validator, as, attrName, values); } } catch (InstantiationException ex) { debug.error("ServiceSchemaImpl.serverEndAttrValidation", ex); String args[] = {javaClass}; throw new SMSException(IUMSConstants.UMS_BUNDLE_NAME, IUMSConstants.SMS_VALIDATOR_CANNOT_INSTANTIATE_CLASS, args); } catch (IllegalAccessException ex) { debug.error("ServiceSchemaImpl.serverEndAttrValidation", ex); String args[] = {javaClass}; throw new SMSException(IUMSConstants.UMS_BUNDLE_NAME, IUMSConstants.SMS_VALIDATOR_CANNOT_INSTANTIATE_CLASS, args); } catch (ClassNotFoundException ex) { debug.error("ServiceSchemaImpl.serverEndAttrValidation", ex); String args[] = {javaClass}; throw new SMSException(IUMSConstants.UMS_BUNDLE_NAME, IUMSConstants.SMS_VALIDATOR_CANNOT_INSTANTIATE_CLASS, args); } } private void validatePlugin( ServiceAttributeValidator validator, AttributeSchemaImpl as, String attrName, Set values ) throws InvalidAttributeValueException { if (!validator.validate(values)) { throwInvalidAttributeValuesException( (validator instanceof RequiredValueValidator), attrName, as); } } private void throwInvalidAttributeValuesException( boolean isRequiredValue, String attrName, AttributeSchemaImpl as) throws InvalidAttributeValueException { String i18nFileName = (ssm != null) ? ssm.getI18NFileName() : null; String message = (isRequiredValue) ? "sms-attribute-values-missing" : "sms-attribute-values-does-not-match-schema"; if (i18nFileName != null) { String[] args = {attrName, i18nFileName, as.getI18NKey()}; throw new InvalidAttributeValueException( IUMSConstants.UMS_BUNDLE_NAME, message, args); } else { String[] args = {attrName}; throw new InvalidAttributeValueException( IUMSConstants.UMS_BUNDLE_NAME, message, args); } } boolean isValid() { return valid; } /** * Returns the I18N properties file name for the service schema. * * @return the I18N properties file name for the service schema */ String getI18NFileName() { return i18nFileName; } }