/*
* ====================
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2008-2009 Sun Microsystems, Inc. All rights reserved.
*
* The contents of this file are subject to the terms of the Common Development
* and Distribution License("CDDL") (the "License"). You may not use this file
* except in compliance with the License.
*
* You can obtain a copy of the License at
* http://IdentityConnectors.dev.java.net/legal/license.txt
* See the License for the specific language governing permissions and limitations
* under the License.
*
* When distributing the Covered Code, include this CDDL Header Notice in each file
* and include the License file at identityconnectors/legal/license.txt.
* If applicable, add the following below this CDDL Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
* ====================
*
* Portions Copyrighted 2013 Forgerock
* Portions Copyrighted 2011 Radovan Semancik (Evolveum)
*/
package org.identityconnectors.ldap.schema;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableSet;
import static org.identityconnectors.common.CollectionUtil.newCaseInsensitiveMap;
import static org.identityconnectors.common.CollectionUtil.newCaseInsensitiveSet;
import static org.identityconnectors.common.CollectionUtil.newReadOnlyList;
import static org.identityconnectors.ldap.LdapEntry.isDNAttribute;
import static org.identityconnectors.ldap.LdapUtil.addBinaryOption;
import static org.identityconnectors.ldap.LdapUtil.getStringAttrValue;
import static org.identityconnectors.ldap.LdapUtil.quietCreateLdapName;
import static org.identityconnectors.ldap.ADLdapUtil.objectGUIDtoString;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.naming.NameAlreadyBoundException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapName;
import org.identityconnectors.common.logging.Log;
import org.identityconnectors.common.security.GuardedString;
import org.identityconnectors.framework.common.exceptions.AlreadyExistsException;
import org.identityconnectors.framework.common.exceptions.ConnectorException;
import org.identityconnectors.framework.common.objects.Attribute;
import org.identityconnectors.framework.common.objects.AttributeBuilder;
import org.identityconnectors.framework.common.objects.AttributeInfo;
import org.identityconnectors.framework.common.objects.AttributeUtil;
import org.identityconnectors.framework.common.objects.Name;
import org.identityconnectors.framework.common.objects.ObjectClass;
import org.identityconnectors.framework.common.objects.ObjectClassInfo;
import org.identityconnectors.framework.common.objects.ObjectClassUtil;
import org.identityconnectors.framework.common.objects.OperationalAttributeInfos;
import org.identityconnectors.framework.common.objects.OperationalAttributes;
import org.identityconnectors.framework.common.objects.Schema;
import org.identityconnectors.framework.common.objects.Uid;
import org.identityconnectors.ldap.ADUserAccountControl;
import org.identityconnectors.ldap.LdapConnection;
import org.identityconnectors.ldap.LdapConstants;
import org.identityconnectors.ldap.LdapEntry;
import org.identityconnectors.ldap.ObjectClassMappingConfig;
/**
* The authoritative description of the mapping between the LDAP schema
* and the connector schema.
*
* @author Andrei Badea
*/
public class LdapSchemaMapping {
private static final Log log = Log.getLog(LdapSchemaMapping.class);
// XXX
// - which attrs returned by default? Currently only userApplications.
// - return binary attrs by default too?
// - type mapping.
// - operations.
// - groups.
// XXX should the naming attribute be present in the schema (e.g. "cn" for account)?
// XXX need a method like getAttributesToReturn(String[] wanted);
// XXX need to check that (extended) naming attributes really exist.
public static final ObjectClass ANY_OBJECT_CLASS = new ObjectClass(ObjectClassUtil.createSpecialName("ANY"));
/**
* The LDAP attribute to map to {@link Name} by default.
*/
static final String DEFAULT_LDAP_NAME_ATTR = "entryDN";
private final LdapConnection conn;
private final Map<String, Set<String>> ldapClass2Effective = newCaseInsensitiveMap();
private Schema schema;
public LdapSchemaMapping(LdapConnection conn) {
this.conn = conn;
}
public Schema schema() {
if (schema == null) {
LdapSchemaBuilder builder = new LdapSchemaBuilder(conn);
if (conn.getConfiguration().getPasswordAttribute() != null) {
// This attribute is already exposed as _PASSWORD_, exposing it also using its
// native name would lead to inconsistencies.
builder.getIgnoredAttrs().add(conn.getConfiguration().getPasswordAttribute());
}
schema = builder.getSchema();
}
return schema;
}
private Set<String> getEffectiveLdapClasses(String ldapClass) {
Set<String> result = ldapClass2Effective.get(ldapClass);
if (result == null) {
result = conn.createNativeSchema().getEffectiveObjectClasses(ldapClass);
ldapClass2Effective.put(ldapClass, result);
}
return result;
}
/**
* Returns the LDAP object classes to which the given framework object
* class is mapped.
*/
public List<String> getLdapClasses(ObjectClass oclass) {
if (oclass.equals(ANY_OBJECT_CLASS)) {
return emptyList();
}
ObjectClassMappingConfig oclassConfig = conn.getConfiguration().getObjectClassMappingConfigs().get(oclass);
if (oclassConfig != null) {
return oclassConfig.getLdapClasses();
}
if (!ObjectClassUtil.isSpecial(oclass)) {
return newReadOnlyList(oclass.getObjectClassValue());
}
throw new ConnectorException("Object class " + oclass.getObjectClassValue() + " is not mapped to an LDAP object class");
}
/**
* Returns the LDAP object class to which the given framework object
* class is mapped in a transitive manner, i.e., together with any superior
* object classes, any superiors thereof, etc..
*/
public Set<String> getEffectiveLdapClasses(ObjectClass oclass) {
Set<String> result = newCaseInsensitiveSet();
for (String ldapClass : getLdapClasses(oclass)) {
result.addAll(getEffectiveLdapClasses(ldapClass));
}
return unmodifiableSet(result);
}
public List<String> getUserNameLdapAttributes(ObjectClass oclass) {
ObjectClassMappingConfig oclassConfig = conn.getConfiguration().getObjectClassMappingConfigs().get(oclass);
if (oclassConfig != null) {
return oclassConfig.getShortNameLdapAttributes();
}
return emptyList();
}
public String getLdapAttribute(ObjectClass oclass, String attrName, boolean transfer) {
String result = null;
if (AttributeUtil.namesEqual(Uid.NAME, attrName)) {
result = getLdapUidAttribute(oclass);
} else if (AttributeUtil.namesEqual(Name.NAME, attrName)) {
result = getLdapNameAttribute(oclass);
} else if (AttributeUtil.namesEqual(OperationalAttributes.PASSWORD_NAME, attrName)){
result = getLdapPasswordAttribute(oclass);
}
if (result == null && !AttributeUtil.isSpecialName(attrName)) {
result = attrName;
}
if (result == null && OperationalAttributes.OPERATIONAL_ATTRIBUTE_NAMES.contains(attrName)) {
if (oclass.equals(ObjectClass.ACCOUNT)){
switch (conn.getServerType()) {
case MSAD_GC:
case MSAD:
result = ADUserAccountControl.MS_USR_ACCT_CTRL_ATTR;
break;
case MSAD_LDS:
//result = ADUserAccountControl.MSDS_USR_ACCT_CTRL_ATTR;
if (OperationalAttributeInfos.ENABLE.is(attrName)) {
result = LdapConstants.MS_DS_USER_ACCOUNT_DISABLED;
} else if (OperationalAttributeInfos.PASSWORD_EXPIRED.is(attrName)) {
result = LdapConstants.MS_DS_USER_PASSWORD_EXPIRED;
} else if (OperationalAttributeInfos.LOCK_OUT.is(attrName)) {
result = LdapConstants.MS_DS_USER_ACCOUNT_AUTOLOCKED;
}
break;
default:
log.warn("Special Attribute {0} of object class {1} is not mapped to an LDAP attribute",
attrName, oclass.getObjectClassValue());
}
}
}
if (result != null && transfer && conn.needsBinaryOption(result)) {
result = addBinaryOption(result);
}
if (result == null && !oclass.equals(ANY_OBJECT_CLASS)) {
log.warn("Attribute {0} of object class {1} is not mapped to an LDAP attribute",
attrName, oclass.getObjectClassValue());
}
return result;
}
/**
* Returns the name of the LDAP attribute which corresponds to the given
* attribute of the given object class, or null.
*/
public String getLdapAttribute(ObjectClass oclass, Attribute attr) {
return getLdapAttribute(oclass, attr.getName(), false);
}
/**
* Returns the names of the LDAP attributes which correspond to the given
* attribute names of the given object class. If {@code transfer} is {@code true},
* the binary option will be added to the attributes which need it.
*/
public Set<String> getLdapAttributes(ObjectClass oclass, Set<String> attrs, boolean transfer) {
Set<String> result = newCaseInsensitiveSet();
for (String attr : attrs) {
String ldapAttr = getLdapAttribute(oclass, attr, transfer);
if (ldapAttr != null) {
result.add(ldapAttr);
}
}
return result;
}
/**
* Returns the LDAP attribute which corresponds to {@link Uid}. Should
* never return null.
*/
public String getLdapUidAttribute(ObjectClass oclass) {
return conn.getConfiguration().getUidAttribute();
}
/**
* Returns the LDAP attribute which corresponds to {@link Name} for the
* given object class. Might return {@code null} if, for example, the
* object class was not configured explicitly in the configuration.
*/
public String getLdapNameAttribute(ObjectClass oclass) {
return DEFAULT_LDAP_NAME_ATTR;
}
public String getLdapPasswordAttribute(ObjectClass oclass) {
return conn.getConfiguration().getPasswordAttribute();
}
/**
* Creates a {@link Uid} for the given entry. It is assumed that the entry
* contains the attribute returned by {@link #getLdapUidAttribute}.
*/
public Uid createUid(ObjectClass oclass, LdapEntry entry) {
return createUid(getLdapUidAttribute(oclass), entry.getAttributes());
}
public Uid createUid(ObjectClass oclass, String entryDN) {
String ldapUidAttr = getLdapUidAttribute(oclass);
if (isDNAttribute(ldapUidAttr)) {
try{
//we do an exact search to get the DN as normalized by the server
NamingEnumeration<SearchResult> ne = conn.getInitialContext().search(entryDN, "objectclass=*", new SearchControls(SearchControls.OBJECT_SCOPE,0,0,null,false,false));
// TODO: ne might be null if entry can not be read back (ACI issues for instances)
SearchResult sr = ne.next();
return new Uid(sr.getNameInNamespace());
} catch (NamingException e) {
throw new ConnectorException(e);
}
} else {
try {
Attributes attributes = conn.getInitialContext().getAttributes(entryDN, new String[] { ldapUidAttr });
return createUid(ldapUidAttr, attributes);
} catch (NamingException e) {
throw new ConnectorException(e);
}
}
}
public Uid createUid(String ldapUidAttr, Attributes attributes) {
String value = null;
if (LdapConstants.MS_GUID_ATTR.equalsIgnoreCase(ldapUidAttr)){
javax.naming.directory.Attribute attr = attributes.get(ldapUidAttr);
if (attr != null) {
value = objectGUIDtoString(attr);
}
}
else{
value = getStringAttrValue(attributes, ldapUidAttr);
}
if (value != null) {
return new Uid(value);
}
throw new ConnectorException("No attribute named " + ldapUidAttr + " found in the search result");
}
/**
* Creates a {@link Name} for the given entry. It is assumed that the entry
* contains the attribute returned by {@link #getLdapNameAttribute}.
*/
public Name createName(ObjectClass oclass, LdapEntry entry) {
String ldapNameAttr = getLdapNameAttribute(oclass);
if (!isDNAttribute(ldapNameAttr)) {
// Not yet implemented.
throw new UnsupportedOperationException("Name can only be mapped to the entry DN");
}
return new Name(entry.getDN().toString());
}
/**
* Returns an empty attribute instead of <code>null</code> when <code>emptyWhenNotFound</code>
* is <code>true</code>.
*/
public Attribute createAttribute(ObjectClass oclass, String attrName, LdapEntry entry, boolean emptyWhenNotFound) {
String ldapAttrNameForTransfer = getLdapAttribute(oclass, attrName, true);
javax.naming.directory.Attribute ldapAttr = null;
if (ldapAttrNameForTransfer != null) {
ldapAttr = entry.getAttributes().get(ldapAttrNameForTransfer);
}
if (ldapAttr == null) {
return emptyWhenNotFound ? AttributeBuilder.build(attrName, emptyList()) : null;
}
AttributeBuilder builder = new AttributeBuilder();
builder.setName(attrName);
try {
NamingEnumeration<?> valEnum = ldapAttr.getAll();
while (valEnum.hasMore()) {
builder.addValue(valEnum.next());
}
} catch (NamingException e) {
throw new ConnectorException(e);
}
return builder.build();
}
public String create(ObjectClass oclass, Name name, javax.naming.directory.Attributes initialAttrs) {
LdapName entryName = quietCreateLdapName(getEntryDN(oclass, name));
BasicAttributes ldapAttrs = new BasicAttributes();
NamingEnumeration<? extends javax.naming.directory.Attribute> initialAttrEnum = initialAttrs.getAll();
while (initialAttrEnum.hasMoreElements()) {
ldapAttrs.put(initialAttrEnum.nextElement());
}
BasicAttribute objectClass = new BasicAttribute("objectClass");
for (String ldapClass : conn.getSchemaMapping().getEffectiveLdapClasses(oclass)) {
objectClass.add(ldapClass);
}
ldapAttrs.put(objectClass);
log.ok("Creating LDAP subcontext {0} with attributes {1}", entryName, ldapAttrs);
try {
conn.getInitialContext().createSubcontext(entryName, ldapAttrs).close();
return entryName.toString();
} catch (NameAlreadyBoundException e){
throw new AlreadyExistsException(e);
} catch (NamingException e) {
throw new ConnectorException(e);
}
}
public javax.naming.directory.Attribute encodeAttribute(ObjectClass oclass, Attribute attr) {
if (attr.is(OperationalAttributes.PASSWORD_NAME)) {
throw new IllegalArgumentException("This method should not be used for password attributes");
}
String ldapAttrName = getLdapAttribute(oclass, attr.getName(), true);
if (ldapAttrName == null) {
return null;
}
final BasicAttribute ldapAttr = new BasicAttribute(ldapAttrName);
List<Object> value = attr.getValue();
if (value != null) {
for (Object each : value) {
ldapAttr.add(each);
}
}
return ldapAttr;
}
public GuardedPasswordAttribute encodePassword(ObjectClass oclass, Attribute attr) {
assert attr.is(OperationalAttributes.PASSWORD_NAME);
String pwdAttrName = conn.getConfiguration().getPasswordAttribute();
List<Object> value = attr.getValue();
if (value != null) {
for (Object each : value) {
GuardedString password = (GuardedString) each;
return GuardedPasswordAttribute.create(pwdAttrName, password);
}
}
return GuardedPasswordAttribute.create(pwdAttrName);
}
public String getEntryDN(ObjectClass oclass, Name name) {
String ldapNameAttr = getLdapNameAttribute(oclass);
if (!isDNAttribute(ldapNameAttr)) {
// Not yet implemented.
throw new UnsupportedOperationException("Name can only be mapped to the entry DN");
}
return name.getNameValue();
}
public String rename(ObjectClass oclass, String entryDN, Name newName) {
String newEntryDN = getEntryDN(oclass, newName);
try {
conn.getInitialContext().rename(entryDN, newEntryDN);
return newEntryDN;
} catch (NameAlreadyBoundException e){
throw new AlreadyExistsException(e);
} catch (NamingException e) {
throw new ConnectorException(e);
}
}
public void removeNonReadableAttributes(ObjectClass oclass, Set<String> attrNames) {
ObjectClassInfo oci = schema().findObjectClassInfo(oclass.getObjectClassValue());
if (oci == null) {
return;
}
Set<String> attrs = newCaseInsensitiveSet();
Set<String> readableAttrs = newCaseInsensitiveSet();
for (AttributeInfo info : oci.getAttributeInfo()) {
String attrName = info.getName();
attrs.add(attrName);
if (info.isReadable()) {
readableAttrs.add(attrName);
}
}
for (Iterator<String> i = attrNames.iterator(); i.hasNext();) {
String attrName = i.next();
// Only remove the attribute if it is a known one. Otherwise
// we could remove attributes that are readable, but not in the schema
// (e.g., LDAP operational attributes).
if (attrs.contains(attrName) && !readableAttrs.contains(attrName)) {
i.remove();
}
}
}
}