/* * 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.dom.api.impl; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Random; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import org.apache.felix.ipojo.annotations.Component; import org.apache.felix.ipojo.annotations.Invalidate; import org.apache.felix.ipojo.annotations.Provides; import org.apache.felix.ipojo.annotations.Requires; import org.apache.felix.ipojo.annotations.Validate; import org.krakenapps.confdb.Config; import org.krakenapps.confdb.ConfigDatabase; import org.krakenapps.confdb.ConfigService; import org.krakenapps.confdb.ConfigTransaction; import org.krakenapps.confdb.ObjectBuilder; import org.krakenapps.confdb.Predicate; import org.krakenapps.confdb.Predicates; import org.krakenapps.dom.api.ConfigManager; import org.krakenapps.dom.api.ConfigUpdateRequest; import org.krakenapps.dom.api.DOMException; import org.krakenapps.dom.api.DefaultEntityEventListener; import org.krakenapps.dom.api.DefaultEntityEventProvider; import org.krakenapps.dom.api.EntityEventListener; import org.krakenapps.dom.api.OrganizationApi; import org.krakenapps.dom.api.OrganizationUnitApi; import org.krakenapps.dom.api.Transaction; import org.krakenapps.dom.api.UserApi; import org.krakenapps.dom.api.UserConfigParser; import org.krakenapps.dom.api.UserExtensionProvider; import org.krakenapps.dom.model.OrganizationUnit; import org.krakenapps.dom.model.User; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceReference; import org.osgi.util.tracker.ServiceTracker; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Component(name = "dom-user-api") @Provides public class UserApiImpl extends DefaultEntityEventProvider<User> implements UserApi { private static final char[] SALT_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".toCharArray(); private final Logger logger = LoggerFactory.getLogger(UserApiImpl.class.getName()); private static final Class<User> cls = User.class; private static final String NOT_FOUND = "user-not-found"; private static final String ALREADY_EXIST = "user-already-exist"; private static final int DEFAULT_SALT_LENGTH = 10; private EntityEventListener<OrganizationUnit> orgUnitEventListener = new DefaultEntityEventListener<OrganizationUnit>() { @Override public void entityRemoving(String domain, OrganizationUnit orgUnit, ConfigTransaction xact, Object state) { List<Config> users = getConfigs(domain, orgUnit.getGuid(), false, null, 0, Integer.MAX_VALUE); Transaction x = Transaction.getInstance(xact); if ((state != null) && (state instanceof Boolean) && ((Boolean) state)) { List<ConfigUpdateRequest<User>> userUpdates = new ArrayList<ConfigUpdateRequest<User>>(); for (Config c : users) { User user = c.getDocument(User.class, cfg.getParseCallback(domain)); user.setOrgUnit(null); userUpdates.add(new ConfigUpdateRequest<User>(c, user)); } updateUsers(domain, userUpdates); } else { FilterByLoginNames filter = new FilterByLoginNames(); for (Config user : users) { @SuppressWarnings("unchecked") Map<String, Object> doc = (Map<String, Object>) user.getDocument(); filter.addLoginName((String) doc.get("login_name")); } cfg.removes(x, domain, cls, Arrays.asList((Predicate) filter), null, UserApiImpl.this); } } }; @Requires private ConfigManager cfg; @Requires private ConfigService conf; @Requires private OrganizationApi orgApi; @Requires private OrganizationUnitApi orgUnitApi; private UserExtensionProviderTracker tracker; private ConcurrentMap<String, UserExtensionProvider> userExtensionProviders = new ConcurrentHashMap<String, UserExtensionProvider>(); public UserApiImpl(BundleContext bc) { this.tracker = new UserExtensionProviderTracker(bc); } @Validate public void validate() { cfg.setParser(User.class, new UserConfigParser()); orgUnitApi.addEntityEventListener(orgUnitEventListener); tracker.open(); } @Invalidate public void invalidate() { if (orgUnitApi != null) orgUnitApi.removeEntityEventListener(orgUnitEventListener); tracker.close(); } private Predicate getPred(String loginName) { return Predicates.field("loginName", loginName); } private List<Predicate> getPreds(List<User> users) { if (users == null) return new ArrayList<Predicate>(); FilterByLoginNames filter = new FilterByLoginNames(); for (User user : users) filter.addLoginName(user.getLoginName()); return Arrays.asList((Predicate) filter); } @Override public int countUsers(String domain, String orgUnitGuid, boolean includeChildren, Predicate pred) { if (orgUnitGuid == null && includeChildren) return cfg.count(domain, cls, null); int total = cfg.count(domain, cls, Predicates.and(Predicates.field("orgUnit/guid", orgUnitGuid), pred)); if (includeChildren) { OrganizationUnit parent = orgUnitApi.getOrganizationUnit(domain, orgUnitGuid); for (OrganizationUnit ou : parent.getChildren()) total += countUsers(domain, ou.getGuid(), includeChildren, pred); } return total; } @Override public Collection<User> getUsers(String domain) { return cfg.all(domain, cls); } @Override public Collection<User> getUsers(String domain, int offset, int limit) { return cfg.all(domain, cls, null, offset, limit); } @Override public Collection<User> getUsers(String domain, Collection<String> loginNames) { return cfg.all(domain, cls, Predicates.in("loginName", loginNames)); } @Override public Collection<User> getUsers(String domain, String orgUnitGuid, boolean includeChildren) { return getUsers(domain, orgUnitGuid, includeChildren, null, 0, Integer.MAX_VALUE); } @Override public Collection<User> getUsers(String domain, String orgUnitGuid, boolean includeChildren, Predicate pred, int offset, int limit) { if (orgUnitGuid == null && includeChildren) return cfg.all(domain, cls, pred, offset, limit); Collection<User> users = cfg.all(domain, cls, Predicates.and(Predicates.field("orgUnit/guid", orgUnitGuid), pred), offset, limit); int dec = Math.min(offset, users.size()); if (offset == dec) // offset <= users limit -= users.size() - offset; offset -= dec; if (includeChildren) { OrganizationUnit parent = orgUnitApi.getOrganizationUnit(domain, orgUnitGuid); for (OrganizationUnit ou : parent.getChildren()) { if (limit <= 0) break; Collection<User> childUsers = getUsers(domain, ou.getGuid(), includeChildren, pred, offset, limit); dec = Math.min(offset, childUsers.size()); if (offset == dec) // offset <= child users limit -= childUsers.size() - offset; offset -= dec; users.addAll(childUsers); } } return users; } @Override public List<Config> getConfigs(String domain, String orgUnitGuid, boolean includeChildren, Predicate pred, int offset, int limit) { if (orgUnitGuid == null && includeChildren) return cfg.matches(domain, cls, pred, offset, limit); List<Config> users = cfg.matches(domain, cls, Predicates.and(Predicates.field("orgUnit/guid", orgUnitGuid), pred), offset, limit); int dec = Math.min(offset, users.size()); if (offset == dec) // offset <= users limit -= users.size() - offset; offset -= dec; if (includeChildren) { OrganizationUnit parent = orgUnitApi.getOrganizationUnit(domain, orgUnitGuid); for (OrganizationUnit ou : parent.getChildren()) { if (limit <= 0) break; List<Config> childUsers = getConfigs(domain, ou.getGuid(), includeChildren, pred, offset, limit); dec = Math.min(offset, childUsers.size()); if (offset == dec) // offset <= child users limit -= childUsers.size() - offset; offset -= dec; users.addAll(childUsers); } } return users; } @Override public Collection<User> getUsers(String domain, String domainController) { return cfg.all(domain, cls, Predicates.field("domainController", domainController)); } @Override public User findUser(String domain, String loginName) { return cfg.find(domain, cls, getPred(loginName)); } @Override public Collection<User> getUsers(String domain, Predicate pred) { return cfg.all(domain, cls, pred); } @Override public User getUser(String domain, String loginName) { return cfg.get(domain, cls, getPred(loginName), NOT_FOUND); } @Override public Collection<String> getLoginNames(String domain, String orgUnitGuid, boolean includeChildren, Predicate pred, int offset, int limit) { if (orgUnitGuid == null && includeChildren) return cfg.findObjects(domain, cls, new LoginNameFetcher(), pred, offset, limit); Collection<String> loginNames = cfg.findObjects(domain, cls, new LoginNameFetcher(), Predicates.and(pred, Predicates.field("orgUnit/guid", orgUnitGuid)), offset, limit); int dec = Math.min(offset, loginNames.size()); if (offset == dec) // offset <= users limit -= loginNames.size() - offset; offset -= dec; if (includeChildren) { OrganizationUnit parent = orgUnitApi.getOrganizationUnit(domain, orgUnitGuid); for (OrganizationUnit ou : parent.getChildren()) { if (limit <= 0) break; Collection<String> childUsers = getLoginNames(domain, ou.getGuid(), includeChildren, pred, offset, limit); dec = Math.min(offset, childUsers.size()); if (offset == dec) // offset <= child users limit -= childUsers.size() - offset; offset -= dec; loginNames.addAll(childUsers); } } return loginNames; } private class LoginNameFetcher implements ObjectBuilder<String> { @Override public String build(Config c) { @SuppressWarnings("unchecked") Map<String, Object> m = (Map<String, Object>) c.getDocument(); String loginName = (String) m.get("login_name"); return loginName; } } @Override public void createUsers(String domain, Collection<User> users) { createUsers(domain, users, false); } @Override public void createUsers(String domain, Collection<User> users, boolean noHash) { if (users == null || users.size() == 0) return; List<User> userList = new ArrayList<User>(users); if (!noHash) { int saltLength = getSaltLength(domain); for (User user : users) { user.setSalt(createSalt(saltLength)); user.setPassword(hashPassword(user.getSalt(), user.getPassword())); } } cfg.adds(domain, cls, getPreds(userList), userList, ALREADY_EXIST, this); } @Override public void createUser(String domain, User user) { createUser(domain, user, false); } @Override public void createUser(String domain, User user, boolean noHash) { if (!noHash) { user.setSalt(createSalt(domain)); user.setPassword(hashPassword(user.getSalt(), user.getPassword())); } cfg.add(domain, cls, getPred(user.getLoginName()), user, ALREADY_EXIST, this); } @Override public void updateUsers(String domain, List<ConfigUpdateRequest<User>> userUpdates) { updateUsers(domain, userUpdates, false); } @Override public void updateUsers(String domain, List<ConfigUpdateRequest<User>> userUpdates, boolean updatePassword) { if (userUpdates == null || userUpdates.size() == 0) return; ConfigDatabase db = conf.ensureDatabase("kraken-dom-" + domain); Transaction xact = new Transaction(domain, db); xact.addEventProvider(cls, this); try { for (ConfigUpdateRequest<User> update : userUpdates) { User user = update.doc; user.setUpdated(new Date()); if (updatePassword) user.setPassword(hashPassword(user.getSalt(), user.getPassword())); xact.update(update.config, user); } xact.commit("kraken-dom", "updated " + userUpdates.size() + " users"); } catch (Throwable e) { xact.rollback(); if (e instanceof DOMException) throw (DOMException) e; throw new RuntimeException(e); } } @Deprecated @Override public void updateUsers(String domain, Collection<User> users, boolean updatePassword) { if (users == null || users.size() == 0) return; List<User> userList = new ArrayList<User>(users); for (User user : users) { user.setUpdated(new Date()); if (updatePassword) user.setPassword(hashPassword(user.getSalt(), user.getPassword())); } cfg.updates(domain, cls, getPreds(userList), userList, NOT_FOUND, this); } @Override public void updateUser(String domain, User user, boolean updatePassword) { // for backward compatibility if (user.getLastPasswordChange() == null) user.setLastPasswordChange(new Date()); user.setUpdated(new Date()); if (updatePassword) user.setPassword(hashPassword(user.getSalt(), user.getPassword())); cfg.update(domain, cls, getPred(user.getLoginName()), user, NOT_FOUND, this); } @Override public void removeUsers(String domain, Collection<String> loginNames) { if (loginNames == null || loginNames.size() == 0) return; Predicate pred = Predicates.in("login_name", new HashSet<String>(loginNames)); cfg.removes(domain, cls, Arrays.asList(pred), NOT_FOUND, this); } @Override public void removeUser(String domain, String loginName) { cfg.remove(domain, cls, getPred(loginName), NOT_FOUND, this); } @Override public Collection<UserExtensionProvider> getExtensionProviders() { return userExtensionProviders.values(); } @Override public UserExtensionProvider getExtensionProvider(String name) { return userExtensionProviders.get(name); } @Override public void setSaltLength(String domain, int length) { if (length < 0 || length > 20) throw new IllegalArgumentException("invalid salt length. (valid: 0~20)"); orgApi.setOrganizationParameter(domain, "salt_length", length); } @Override public int getSaltLength(String domain) { Object length = orgApi.getOrganizationParameter(domain, "salt_length"); if (length == null || !(length instanceof Integer)) return DEFAULT_SALT_LENGTH; return (Integer) length; } @Override public String createSalt(String domain) { int saltLength = getSaltLength(domain); logger.trace("kraken dom: salt length [{}]", saltLength); return createSalt(saltLength); } private String createSalt(int saltLength) { StringBuilder salt = new StringBuilder(saltLength); Random rand = new Random(); for (int i = 0; i < saltLength; i++) salt.append(SALT_CHARS[rand.nextInt(SALT_CHARS.length)]); return salt.toString(); } @Override public boolean verifyPassword(String domain, String loginName, String password) { User user = getUser(domain, loginName); String hash = hashPassword(user.getSalt(), password); // null check if (user.getPassword() == null || hash == null) return (password == hash); return user.getPassword().equals(hash); } @Override public String hashPassword(String salt, String text) { return Sha1.hashPassword(salt, text); } private class UserExtensionProviderTracker extends ServiceTracker { public UserExtensionProviderTracker(BundleContext bc) { super(bc, UserExtensionProvider.class.getName(), null); } @Override public Object addingService(ServiceReference reference) { UserExtensionProvider p = (UserExtensionProvider) super.addingService(reference); userExtensionProviders.put(p.getExtensionName(), p); return p; } @Override public void removedService(ServiceReference reference, Object service) { UserExtensionProvider p = (UserExtensionProvider) service; userExtensionProviders.remove(p.getExtensionName()); super.removedService(reference, service); } } private static class FilterByLoginNames implements Predicate { private HashSet<String> loginNames = new HashSet<String>(); public void addLoginName(String loginName) { loginNames.add(loginName); } @Override public boolean eval(Config c) { Object doc = c.getDocument(); if (doc == null) return false; @SuppressWarnings("unchecked") Map<String, Object> m = (Map<String, Object>) doc; return loginNames.contains(m.get("login_name")); } } }