/*
* ====================
* 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]"
* ====================
*/
package org.identityconnectors.racf;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import org.identityconnectors.framework.common.exceptions.ConnectorException;
import org.identityconnectors.framework.common.objects.ConnectorObject;
import org.identityconnectors.framework.common.objects.ObjectClass;
import org.identityconnectors.framework.common.objects.OperationOptions;
import org.identityconnectors.framework.common.objects.SyncDeltaBuilder;
import org.identityconnectors.framework.common.objects.SyncDeltaType;
import org.identityconnectors.framework.common.objects.SyncResultsHandler;
import org.identityconnectors.framework.common.objects.SyncToken;
import org.identityconnectors.framework.common.objects.Uid;
public class SyncUtil {
private RacfConnector _connector;
public SyncUtil(RacfConnector connector) {
_connector = connector;
Map<String, Object> geo = getRootDSE();
if (!geo.containsKey("changelog")
|| !geo.containsKey("lastchangenumber")
|| !geo.containsKey("firstchangenumber")) {
String error = "Unable to locate the RACF change log.";
throw new ConnectorException(error);
}
}
public SyncToken getLatestSyncToken(ObjectClass objClass) {
Map<String, Object> geo = getRootDSE();
if (geo != null)
return new SyncToken((String)geo.get("lastchangenumber"));
return null;
}
public void sync(ObjectClass objClass, SyncToken token,
SyncResultsHandler handler, OperationOptions options) {
String maxChangeNumber = null;
if (token!=null && token.getValue() != null) {
maxChangeNumber = (String) token.getValue();
}
// either set the default to the current highest value or null
// for no filter (get everyone).
//
// PWD: set to current min change number, not 0, if resetting.
if (maxChangeNumber == null) {
Boolean resetToToday = ((RacfConfiguration)_connector.getConfiguration()).getActiveSyncResetToToday();
if (resetToToday==null)
resetToToday = false;
if (resetToToday.booleanValue()) {
Map<String, Object> geo = getRootDSE();
if (geo != null)
maxChangeNumber = (String)geo.get("lastchangenumber");
} else {
Map<String, Object> geo = getRootDSE();
if (geo != null)
maxChangeNumber = (String)geo.get("firstchangenumber");
}
if (maxChangeNumber == null)
maxChangeNumber = "0";
}
// Filter based on the person who made the change.
//
List<String> nameFilter = null;
String[] attr = ((RacfConfiguration)_connector.getConfiguration()).getActiveSyncFilterChangesBy();
if (attr != null && attr.length > 0)
nameFilter = Arrays.asList(attr);
// Search Context
//
String context = null;
Map<String, Object> dse = getRootDSE();
if (dse != null)
context = (String) dse.get("changelog");
if (context == null)
context = "cn=changelog";
String[] attributesToGet = {
"targetdn",
"changetype",
"changes",
"changetime",
"changenumber",
"ibm-changeinitiatorsname" // RACF LDAP uses this attribute for the modifiersname.
};
try {
SearchControls subTreeControls = new SearchControls(SearchControls.ONELEVEL_SCOPE, 4095, 0, attributesToGet, true, true);
NamingEnumeration<SearchResult> entries = ((RacfConnection)_connector.getConnection()).getDirContext().search(context, getFilter(maxChangeNumber), subTreeControls);
// Iterate through the entries, filtering and then applying changes
//
while (entries.hasMore()) {
SearchResult entry = entries.next();
Attributes attributes = entry.getAttributes();
{
Object value = getAttributeValueFromLdap(attributes, "changenumber");
if (value != null) {
int cur = atoi(value.toString(), -1);
int max = atoi(maxChangeNumber, -1);
if (cur > max)
maxChangeNumber = value.toString();
}
}
// Filter changes by person who changed, if enabled
//
boolean filterOut = false;
Map<String, Object> changes = new TreeMap<String, Object>(new StringComparator());
addChangeLogAttributes(changes, entry);
//GAEL: IBM does differently, "modifiersname" is not in the changes but in the
// attribute ibm-changeinitiatorsname
if (nameFilter != null) {
String modifier = (String) getAttributeValueFromLdap(attributes, "ibm-changeinitiatorsname");
if (modifier != null) {
for (int j = 0; j < nameFilter.size(); j++) {
String listedFilter = (String) nameFilter.get(j);
if (listedFilter.trim().equalsIgnoreCase(modifier.trim())) {
filterOut = true;
break;
}
}
}
}
if (!filterOut) {
LocalHandler localHandler = new LocalHandler();
javax.naming.directory.Attribute targetDn = attributes.get("targetdn");
Object changeType = getAttributeValueFromLdap(attributes, "changetype");
String change = changeType==null?null:changeType.toString();
if ("DELETE".equalsIgnoreCase(change)) {
SyncDeltaBuilder builder = new SyncDeltaBuilder();
builder.setDeltaType(SyncDeltaType.DELETE);
builder.setUid(new Uid(LdapUtil.createUniformUid(targetDn.get().toString(), ((RacfConfiguration)_connector.getConfiguration()).getSuffix())));
builder.setToken(new SyncToken(maxChangeNumber));
handler.handle(builder.build());
} else if ("ADD".equalsIgnoreCase(change) || "MODIFY".equalsIgnoreCase(change)) {
String query = "(racfid="+RacfConnector.extractRacfIdFromLdapId(targetDn.get().toString())+")";
_connector.executeQuery(ObjectClass.ACCOUNT, query, localHandler, options);
if (localHandler.size()>0) {
ConnectorObject user = localHandler.iterator().next();
//TODO: for password sync, do we want to check if this object
// has a password?
SyncDeltaBuilder builder = new SyncDeltaBuilder();
builder.setDeltaType(SyncDeltaType.CREATE_OR_UPDATE);
builder.setObject(user);
builder.setToken(new SyncToken(maxChangeNumber));
handler.handle(builder.build());
}
}
}
}
} catch (NamingException ne) {
throw ConnectorException.wrap(ne);
}
}
private Map<String, Object> getRootDSE() {
try {
Set<String> attributesToGet = new HashSet<String>();
attributesToGet.add("changelog");
attributesToGet.add("firstchangenumber");
attributesToGet.add("lastchangenumber");
Map<String,Object> attributesRead = new HashMap<String, Object>();
getAttributesFromLdap("", attributesRead, attributesToGet, SearchControls.OBJECT_SCOPE);
return attributesRead;
} catch (NamingException ne) {
throw ConnectorException.wrap(ne);
}
}
private SearchResult getAttributesFromLdap(String ldapName, Map<String, Object> attributesRead,
Set<String> attributesToGet, int scope) throws NamingException {
SearchControls subTreeControls = new SearchControls(scope, 4095, 0, attributesToGet==null?null:attributesToGet.toArray(new String[0]), true, true);
SearchResult ldapObject = null;
NamingEnumeration<SearchResult> results = _connector.getConnection().getDirContext().search(ldapName, "(objectclass=*)", subTreeControls);
if (!results.hasMoreElements())
return null;
ldapObject = results.next();
Attributes attributes = ldapObject.getAttributes();
NamingEnumeration<? extends javax.naming.directory.Attribute> attributeEnum = attributes.getAll();
while (attributeEnum.hasMore()) {
javax.naming.directory.Attribute attribute = attributeEnum.next();
attributesRead.put(attribute.getID(), LdapUtil.getValueFromAttribute(attribute));
}
return ldapObject;
}
private Object getAttributeValueFromLdap(Attributes attributes, String attributeName)
throws NamingException {
javax.naming.directory.Attribute entryChangeNumber = attributes.get(attributeName);
Object value = entryChangeNumber==null?null:entryChangeNumber.get();
return value;
}
private String getFilter(String maxChangeNumber) {
// build a search string for entries after our changenumber. limit to
// blocksize.
String blockSize = ((RacfConfiguration)_connector.getConfiguration()).getActiveSyncBlocksize();
String attrName = "changenumber";
Boolean useORSearch = ((RacfConfiguration)_connector.getConfiguration()).getActiveSyncFilterUseOrSearch();
if (useORSearch==null)
useORSearch = false;
// Adding the objectClass to the search can decrease performance let the
// customer decide (not available via the GUI)
Boolean removeObjectClassSearch = ((RacfConfiguration)_connector.getConfiguration()).getActiveSyncRemoveOCFromFilter();
if (removeObjectClassSearch==null)
removeObjectClassSearch = false;
// new min is old one plus one.
int max = Integer.parseInt(maxChangeNumber);
int endNumber = 0;
StringBuffer filter = new StringBuffer();
if (blockSize != null && useORSearch) {
// remove the objectClass completely from the search
if (!removeObjectClassSearch) {
filter.append("(&(objectClass=changelogentry)");
} // if removeObjectClassSearch
filter.append("(|(");
filter.append(attrName);
filter.append("=");
filter.append(Integer.toString(max + 1));
filter.append(')');
try {
endNumber = Integer.parseInt(maxChangeNumber) + Integer.parseInt(blockSize);
} catch (NumberFormatException ne) {
// nothing
}
if (endNumber > 0) {
for (int i = (max + 2); i <= endNumber; i++) {
filter.append("(");
filter.append(attrName);
filter.append("=");
filter.append(Integer.toString(i));
filter.append(')');
} // for i=max+2..endNumber
} // if endNumber > 0
if (!removeObjectClassSearch) {
filter.append(")");
} // if removeObjectClassSearch
filter.append(")");
} else {
filter.append("(&(");
// remove the objectClass completely from the search
if (!removeObjectClassSearch) {
filter.append("objectClass=changelogentry)(");
} // if removeObjectClassSearch
filter.append(attrName);
filter.append(">=");
filter.append(Integer.toString(max + 1));
filter.append(')');
if (blockSize != null) {
try {
endNumber = Integer.parseInt(maxChangeNumber) + Integer.parseInt(blockSize);
} catch (NumberFormatException ne) {
}
if (endNumber > 0) {
filter.append("(");
filter.append(attrName);
filter.append("<=");
filter.append(endNumber);
filter.append(')');
}
}
filter.append(')');
}
return filter.toString();
}
private void addChangeLogAttributes(Map<String, Object> map, SearchResult changeLogEntry) throws NamingException {
Attributes attributes = changeLogEntry.getAttributes();
String changeType = (String) getAttributeValueFromLdap(attributes, "changetype");
map.put("changeType", changeType);
map.put("changeNumber", getAttributeValueFromLdap(attributes, "changenumber"));
map.put("targetDN", getAttributeValueFromLdap(attributes, "targetdn"));
map.put("identity", getAttributeValueFromLdap(attributes, "targetdn"));
//map.put("changes", getAttributeValueFromLdap(attributes, "changes"));
/**
* parse out the attribute names that have changed. This is an LDIF
* formatted record, which are newline terminated lines of the format
* keyword:value.
*/
if (changeType.equals("MODIFY")) {
// GAEL:
//The changelog attribute "changes" is only present when a RACF user password is changed,
// and will contain: replace: racfPassword racfPassword: *ComeAndGetIt* -
Object changes = getAttributeValueFromLdap(attributes, "changes");
if (changes != null) {
map.put("changes", changes);
Map<String, Object> changeMap = new TreeMap<String, Object>(new StringComparator());
StringTokenizer st = new StringTokenizer((String) changes, "\n");
while (st.hasMoreTokens()) {
String line = st.nextToken();
int sepIndex = line.indexOf(':');
if (sepIndex > 0) {
String op = line.substring(0, sepIndex).trim();
if (op.equals("replace")) {
// Only replace operation can appear for the password
Object[] val = getLDIFAttributeValue(st);
if (val != null) {
// here the value should be (has to be) *ComeAndGetIt*
// GAEL: Check it in case... but well, it can't be anything else at that point
changeMap.put((String)val[0], val[1]);
}
}
}
map.put("changedAttributes", changeMap);
}
}
} // if changeType modify
}
private Object[] getLDIFAttributeValue(StringTokenizer st) {
List<String> list = new ArrayList<String>();
String line = st.nextToken();
String name = null;
while (line != null && !line.startsWith("-")) {
// If this line starts with a space, then it is a continuation
// of the previous line, so modify the previous value by
// appending the additional value data.
if ( line.startsWith(" ") ) {
// if list.size() == 0, then this is illegal LDIF, so we
// don't cover that case.
if ( list.size() > 0 ) {
int i = list.size() - 1;
String value = list.get(i) + line.substring(1);
list.set(i, value.trim());
}
} else {
int sepIndex = line.indexOf(':');
if (sepIndex > 0) {
String key = line.substring(0, sepIndex);
String value = line.substring(sepIndex + 1);
name = key.trim();
list.add(value.trim());
}
}
if (st.hasMoreTokens())
line = st.nextToken();
else
break;
}
//
// The name has been set. Set the value to either the single value or
// the list.
//
if (list.size() == 0) {
return null;
} else if (list.size() == 1) {
return new Object[] {name, parseLDIFValue(name, list.get(0), 0)};
} else {
List<Object> decodedList = new ArrayList<Object>(list.size());
for (String value : list) {
decodedList.add(parseLDIFValue(name, value, 0));
}
return new Object[] {name, decodedList};
}
}
private Object parseLDIFValue(String attributeName, String line, int pos) {
if (pos >= line.length()) {
return "";
}
return line.substring(pos).trim();
}
private int atoi(String a, int def) {
int i = def;
if (a != null && a.length() > 0) {
try {
int decimal = a.indexOf('.');
if (decimal > 0)
a = a.substring(0, decimal);
i = Integer.parseInt(a);
} catch (NumberFormatException e) {
// ignore, return default
}
}
return i;
}
static public class StringComparator implements java.util.Comparator<String>, Serializable {
public int compare(String key1, String key2) {
// the arrays must be non-null but the cells may be null
if (key1!=null && key2!=null) return key1.compareToIgnoreCase(key2);
else if (key1 == null && key2 == null) return 0;
else if (key1 == null) return -1;
else return 1;
}
}
}