/**
* 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 <code>ServiceSchema</code> 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<String, AttributeSchemaImpl> attrSchemas;
Map attrValidators;
Map attrDefaults;
Map<String, Set<String>> attributeExamples;
Map attrReadOnlyDefaults;
Map<String, ServiceSchemaImpl> 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 <code>true</code> if service schema supports multiple
* configurations; <code>false</code> 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<String> getAttributeSchemaNames() {
return new HashSet<String>(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<AttributeSchemaImpl> getAttributeSchemas() {
return new HashSet<AttributeSchemaImpl>(attrSchemas.values());
}
/**
* Get a map of all the attribute and their default values in this schema
*/
Map<String, Set<String>> 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<String> getSubSchemaNames() {
return new HashSet<>(subSchemas.keySet());
}
/**
* Returns <code>ServiceSchema</code> object given the name of the
* service's sub-schema.
*/
ServiceSchemaImpl getSubSchema(String subSchemaName) throws SMSException {
return ((ServiceSchemaImpl) subSchemas.get(subSchemaName));
}
/**
* Returns <code>ServiceSchema</code> for creating Organizations if
* present; else <code>null</code>.
*/
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<String, Set<String>> 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<String, Set<String>> 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<String, Set<String>> 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<String, Set<String>> 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<String> 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<String> 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<String, Set<String>> 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<String> 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<String, AttributeSchemaImpl> newAttrSchemas = new HashMap<>();
Map newAttrValidators = new HashMap();
Map newAttrDefaults = new HashMap();
Map<String, Set<String>> newAttrExamples = new HashMap<>();
Map<String, ServiceSchemaImpl> 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 <code>Set</code> 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;
}
}