/*
* Copyright 2011 Future Systems
*
* 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 org.krakenapps.ldap.impl;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.felix.ipojo.annotations.Component;
import org.apache.felix.ipojo.annotations.Provides;
import org.apache.felix.ipojo.annotations.Requires;
import org.krakenapps.api.FieldOption;
import org.krakenapps.api.PrimitiveConverter;
import org.krakenapps.cron.PeriodicJob;
import org.krakenapps.dom.api.AdminApi;
import org.krakenapps.dom.api.OrganizationUnitApi;
import org.krakenapps.dom.api.ProgramApi;
import org.krakenapps.dom.api.RoleApi;
import org.krakenapps.dom.api.UserApi;
import org.krakenapps.dom.model.Admin;
import org.krakenapps.dom.model.OrganizationUnit;
import org.krakenapps.dom.model.User;
import org.krakenapps.ldap.LdapOrgUnit;
import org.krakenapps.ldap.LdapProfile;
import org.krakenapps.ldap.LdapService;
import org.krakenapps.ldap.LdapSyncService;
import org.krakenapps.ldap.LdapUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@PeriodicJob("* * * * *")
@Component(name = "ldap-sync-service")
@Provides
public class DomSyncService implements LdapSyncService, Runnable {
private static final String EXT_NAME = "ldap";
private static final String DEFAULT_ROLE_NAME = "admin";
private static final String DEFAULT_PROFILE_NAME = "all";
private final Logger logger = LoggerFactory.getLogger(DomSyncService.class);
@Requires
private LdapService ldap;
@Requires
private UserApi userApi;
@Requires
private AdminApi adminApi;
@Requires
private OrganizationUnitApi orgUnitApi;
@Requires
private RoleApi roleApi;
@Requires
private ProgramApi programApi;
@Override
public void run() {
try {
syncAll();
} catch (Exception e) {
logger.error("kraken ldap: sync error", e);
}
}
private void syncAll() {
// NPE at stopping
if (ldap == null)
return;
for (LdapProfile profile : ldap.getProfiles()) {
try {
if (profile.getSyncInterval() == 0)
continue;
Date d = profile.getLastSync();
Date now = new Date();
long span = -1;
if (d != null)
span = now.getTime() - d.getTime();
if (span == -1 || span > profile.getSyncInterval()) {
try {
logger.debug("kraken ldap: try to sync using profile [{}]", profile.getName());
sync(profile);
} catch (Exception e) {
logger.error("kraken ldap: periodic sync failed", e);
}
profile.setLastSync(now);
ldap.updateProfile(profile);
}
} catch (Exception e) {
logger.error("kraken ldap: sync profile " + profile.getName() + " failed", e);
}
}
}
@Override
public void sync(LdapProfile profile) {
Collection<LdapOrgUnit> orgUnits = ldap.getOrgUnits(profile);
Collection<LdapUser> users = ldap.getUsers(profile);
exportDom(orgUnits, users, profile);
profile.setLastSync(new Date());
}
@Override
public void unsync(LdapProfile profile) {
Collection<OrganizationUnit> orgUnits = orgUnitApi.getOrganizationUnits("localhost");
Collection<String> guids = new ArrayList<String>();
for (OrganizationUnit orgUnit : orgUnits) {
Map<String, Object> ext = orgUnit.getExt();
if (ext.containsKey(EXT_NAME)) {
Map<String, Object> ldap = (Map<String, Object>) ext.get(EXT_NAME);
if (ldap.get("profile").toString().equals(profile.getName().toString())) {
logger.debug("kraken-ldap: ext ldap profile name=[{}], ldap profile name=[{}]", ldap.get("profile"),
profile.getName());
String guid = orgUnit.getGuid();
guids.add(guid);
}
}
}
orgUnitApi.removeOrganizationUnits("localhost", guids);
}
@Override
public void unsyncAll() {
Collection<OrganizationUnit> orgUnits = orgUnitApi.getOrganizationUnits("localhost");
Collection<String> guids = new ArrayList<String>();
for (OrganizationUnit orgUnit : orgUnits) {
Map<String, Object> ext = orgUnit.getExt();
if (ext.containsKey(EXT_NAME)) {
String guid = orgUnit.getGuid();
guids.add(guid);
}
}
orgUnitApi.removeOrganizationUnits("localhost", guids);
}
private void exportDom(Collection<LdapOrgUnit> orgUnits, Collection<LdapUser> users, LdapProfile profile) {
Map<List<String>, OrganizationUnit> domOrgUnits = exportOrgUnits(profile, orgUnits);
exportUsers(profile, users, domOrgUnits);
}
@SuppressWarnings("unchecked")
private Map<List<String>, OrganizationUnit> exportOrgUnits(LdapProfile profile, Collection<LdapOrgUnit> orgUnits) {
String domain = profile.getTargetDomain();
Map<List<String>, OrganizationUnit> result = new HashMap<List<String>, OrganizationUnit>();
// names-object map
Map<List<String>, LdapOrgUnit> orgUnitsMap = new HashMap<List<String>, LdapOrgUnit>();
for (LdapOrgUnit orgUnit : orgUnits) {
// only active directory has org unit distinguished name
String dn = orgUnit.getDistinguishedName();
if (dn == null)
continue;
orgUnitsMap.put(getOUs(dn), orgUnit);
}
// sync
Set<OrganizationUnit> remove = new HashSet<OrganizationUnit>();
Set<String> removeGuids = new HashSet<String>();
for (OrganizationUnit ou : orgUnitApi.getOrganizationUnits(domain)) {
Object ext = ou.getExt().get(EXT_NAME);
if (ext != null && ext instanceof Map && profile.getName().equals(((Map<String, Object>) ext).get("profile"))) {
remove.add(ou);
removeGuids.add(ou.getGuid());
}
}
List<OrganizationUnit> create = new ArrayList<OrganizationUnit>();
Map<String, OrganizationUnit> roots = new HashMap<String, OrganizationUnit>();
List<OrganizationUnit> update = new ArrayList<OrganizationUnit>();
for (List<String> names : orgUnitsMap.keySet()) {
LdapOrgUnit orgUnit = orgUnitsMap.get(names);
OrganizationUnit domOrgUnit = createOrgUnits(domain, create, roots, names);
Map<String, Object> ext = (Map<String, Object>) PrimitiveConverter.serialize(orgUnit);
ext.put("profile", profile.getName());
Map<String, Object> domOrgUnitExt = domOrgUnit.getExt();
if (domOrgUnitExt == null)
domOrgUnitExt = new HashMap<String, Object>();
else if (!create.contains(domOrgUnit) && !domOrgUnitExt.containsKey(EXT_NAME)) {
logger.trace("kraken ldap: skip local org unit [{}, {}]", domOrgUnit.getName(), domOrgUnit.getGuid());
continue;
}
domOrgUnitExt.put(EXT_NAME, ext);
Object before = domOrgUnit.getExt().get(EXT_NAME);
Object after = domOrgUnitExt.get(EXT_NAME);
boolean equals = after.equals(before);
domOrgUnit.setExt(domOrgUnitExt);
if (!create.contains(domOrgUnit)) {
if (!equals)
update.add(domOrgUnit);
}
result.put(names, domOrgUnit);
removeGuids.remove(domOrgUnit.getGuid());
}
for (OrganizationUnit ou : remove) {
if (!removeGuids.contains(ou.getGuid()))
continue;
if (hasLocalOU(ou, profile.getName())) {
removeGuids.remove(ou.getGuid());
ou.getExt().remove(EXT_NAME);
update.add(ou);
}
}
orgUnitApi.createOrganizationUnits(domain, create);
orgUnitApi.updateOrganizationUnits(domain, update);
orgUnitApi.removeOrganizationUnits(domain, removeGuids, true);
return result;
}
@SuppressWarnings("unchecked")
private boolean hasLocalOU(OrganizationUnit ou, String profileName) {
Map<String, Object> ext = ou.getExt();
if (ext == null || !ext.containsKey(EXT_NAME))
return true;
Object ldap = ext.get(EXT_NAME);
if (!(ldap instanceof Map))
return true;
if (!profileName.equals(((Map<String, Object>) ldap).get("profile")))
return true;
for (OrganizationUnit child : ou.getChildren()) {
if (hasLocalOU(child, profileName))
return true;
}
return false;
}
private OrganizationUnit createOrgUnits(String domain, List<OrganizationUnit> create, Map<String, OrganizationUnit> roots,
List<String> names) {
if (names.isEmpty())
return null;
ListIterator<String> nameit = names.listIterator(names.size());
String rootname = nameit.previous();
OrganizationUnit orgUnit = roots.get(rootname);
if (orgUnit == null) {
orgUnit = orgUnitApi.findOrganizationUnitByName(domain, rootname);
if (orgUnit == null) {
orgUnit = new OrganizationUnit();
orgUnit.setName(rootname);
create.add(orgUnit);
}
roots.put(rootname, orgUnit);
}
while (nameit.hasPrevious()) {
String name = nameit.previous();
OrganizationUnit parent = orgUnit;
orgUnit = null;
for (OrganizationUnit child : parent.getChildren()) {
if (name.equals(child.getName())) {
orgUnit = child;
break;
}
}
if (orgUnit == null) {
orgUnit = new OrganizationUnit();
orgUnit.setName(name);
orgUnit.setParent(parent.getGuid());
create.add(orgUnit);
parent.getChildren().add(orgUnit);
}
}
return orgUnit;
}
private static class DomUserConstraints {
static public int lenLoginName;
static public int lenName;
static public int lenTitle;
static public int lenEmail;
static public int lenPhone;
static {
try {
lenLoginName = getLength("loginName");
lenName = getLength("name");
lenTitle = getLength("title");
lenEmail = getLength("email");
lenPhone = getLength("phone");
} catch (SecurityException e) {
LoggerFactory.getLogger(DomSyncService.class).warn("kraken ldap: can't load field length limit", e);
} catch (NoSuchFieldException e) {
LoggerFactory.getLogger(DomSyncService.class).warn("kraken ldap: can't load field length limit", e);
}
}
private static int getLength(String string) throws SecurityException, NoSuchFieldException {
Field declaredField = User.class.getDeclaredField("loginName");
FieldOption annotation = declaredField.getAnnotation(FieldOption.class);
if (annotation != null)
return annotation.length();
else
return 0;
}
}
@SuppressWarnings("unchecked")
private void exportUsers(LdapProfile profile, Collection<LdapUser> users, Map<List<String>, OrganizationUnit> orgUnits) {
String domain = profile.getTargetDomain();
// remove removed ldap users
Set<String> loginNames = new HashSet<String>();
List<String> remove = new ArrayList<String>();
Map<String, User> domUsers = new HashMap<String, User>();
for (LdapUser user : users)
loginNames.add(user.getAccountName());
for (User user : userApi.getUsers(domain)) {
domUsers.put(user.getLoginName(), user);
Object ext = user.getExt().get(EXT_NAME);
if (ext == null || !(ext instanceof Map) || !profile.getName().equals(((Map<String, Object>) ext).get("profile")))
continue;
if (!loginNames.contains(user.getLoginName()))
remove.add(user.getLoginName());
}
Admin defaultAdmin = new Admin();
defaultAdmin.setRole(roleApi.getRole(domain, DEFAULT_ROLE_NAME));
defaultAdmin.setProfile(programApi.getProgramProfile(domain, DEFAULT_PROFILE_NAME));
defaultAdmin.setIdleTimeout(3600);
defaultAdmin.setLoginLockCount(5);
defaultAdmin.setUseLoginLock(true);
defaultAdmin.setUseIdleTimeout(true);
defaultAdmin.setUseOtp(false);
defaultAdmin.setUseAcl(false);
defaultAdmin.setEnabled(true);
// sync
List<User> create = new ArrayList<User>();
List<User> update = new ArrayList<User>();
List<Object[]> failed = new ArrayList<Object[]>();
for (LdapUser user : users) {
User domUser = domUsers.get(user.getAccountName());
boolean exist = (domUser != null);
Map<String, Object> ext = (Map<String, Object>) PrimitiveConverter.serialize(user);
ext.put("profile", profile.getName());
ext.remove("account_name");
ext.remove("logon_count");
ext.remove("last_logon");
boolean basicInfoUpdated = false;
try {
if (domUser == null)
domUser = new User();
else if (domUser.getExt() == null || !domUser.getExt().containsKey(EXT_NAME)) {
logger.trace("kraken ldap: skip local user [{}]", domUser.getLoginName());
continue;
}
basicInfoUpdated = updateDomUserFromDomainUser(orgUnits, user, domUser);
} catch (Exception e) {
logger.trace("kraken ldap: update failed", e);
failed.add(new Object[] { user, e });
continue;
}
Map<String, Object> domUserExt = domUser.getExt();
if (domUserExt == null)
domUserExt = new HashMap<String, Object>();
domUserExt.put(EXT_NAME, ext);
if (user.isDomainAdmin() && !domUserExt.containsKey(adminApi.getExtensionName()))
domUserExt.put(adminApi.getExtensionName(), defaultAdmin);
boolean equals = domUserExt.equals(domUser.getExt());
domUser.setExt(domUserExt);
if (!exist) {
create.add(domUser);
logger.trace("kraken ldap: dom user [{}] will be created", domUser.getLoginName());
} else {
if (!equals || basicInfoUpdated) {
update.add(domUser);
logger.trace("kraken ldap: dom user [{}] will be updated", domUser.getLoginName());
}
}
}
if (!failed.isEmpty()) {
int reportId = new Object().hashCode();
logger.trace(
"kraken ldap: Importing some accounts failed and ignored while syncing in DomSyncService. failure report identifier: {}",
reportId);
for (Object[] f : failed) {
LdapUser acc = (LdapUser) f[0];
Exception e = (Exception) f[1];
logger.trace("kraken ldap: {}: {}", reportId, String.format("%s: %s", acc.getDistinguishedName(), e.toString()));
}
}
userApi.createUsers(domain, create);
userApi.updateUsers(domain, update, false);
userApi.removeUsers(domain, remove);
}
private static Pattern p = Pattern.compile("OU=(.*?),");
private List<String> getOUs(String dn) {
Matcher m = p.matcher(dn);
List<String> ous = new ArrayList<String>();
while (m.find())
ous.add(m.group(1));
return ous;
}
/**
* @return true if user data updated
*/
private boolean updateDomUserFromDomainUser(Map<List<String>, OrganizationUnit> orgUnits, LdapUser user, User domUser) {
boolean updated = false;
domUser.setSourceType("ldap");
if (domUser.getLoginName() == null || !domUser.getLoginName().equals(user.getAccountName())) {
domUser.setLoginName(user.getAccountName());
updated = true;
}
List<String> ou = getOUs(user.getDistinguishedName());
String ouguid = orgUnits.containsKey(ou) ? orgUnits.get(ou).getGuid() : null;
if (domUser.getOrgUnit() == null || !domUser.getOrgUnit().getGuid().equals(ouguid)) {
if (ouguid != null) {
domUser.setOrgUnit(orgUnits.get(ou));
updated = true;
}
}
if (domUser.getName() == null || !domUser.getName().equals(user.getDisplayName())) {
domUser.setName(user.getDisplayName());
updated = true;
}
if (domUser.getTitle() == null || !domUser.getTitle().equals(user.getTitle())) {
if (user.getTitle() != null) {
if (DomUserConstraints.lenTitle < user.getTitle().length())
logger.trace("kraken ldap: title longer than {}: {}", DomUserConstraints.lenTitle, domUser.getTitle());
else {
domUser.setTitle(user.getTitle());
updated = true;
}
}
}
if (domUser.getEmail() == null || !domUser.getEmail().equals(user.getMail())) {
if (user.getMail() != null) {
if (DomUserConstraints.lenEmail < user.getMail().length())
logger.trace("kraken ldap: email longer than {}: {}", DomUserConstraints.lenEmail, domUser.getEmail());
else {
domUser.setEmail(user.getMail());
updated = true;
}
}
}
if (domUser.getPhone() == null || !domUser.getPhone().equals(user.getMobile())) {
if (user.getMobile() != null) {
if (DomUserConstraints.lenPhone < user.getMobile().length())
logger.trace("kraken ldap: phone longer than {}: {}", DomUserConstraints.lenPhone, domUser.getPhone());
else {
domUser.setPhone(user.getMobile());
updated = true;
}
}
}
if (updated) {
checkConstraints(domUser);
return true;
} else {
return false;
}
}
private void checkConstraints(User domUser) {
if (domUser.getLoginName() != null && DomUserConstraints.lenLoginName < domUser.getLoginName().length())
throw new IllegalArgumentException(String.format("getLoginName longer than %d: %s", DomUserConstraints.lenLoginName,
domUser.getLoginName()));
if (domUser.getName() != null && DomUserConstraints.lenName < domUser.getName().length())
throw new IllegalArgumentException(String.format("getName longer than %d: %s", DomUserConstraints.lenName,
domUser.getName()));
}
}