/* * eXist Open Source Native XML Database * Copyright (C) 2001-2015 The eXist Project * http://exist-db.org * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package org.exist.security.internal; import org.exist.scheduler.JobDescription; import org.exist.security.AbstractRealm; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.exist.Database; import org.exist.EXistException; import org.exist.collections.Collection; import org.exist.config.Configuration; import org.exist.config.Configurator; import org.exist.config.ConfigurationException; import org.exist.config.annotation.*; import org.exist.dom.persistent.DocumentImpl; import org.exist.security.AuthenticationException; import org.exist.security.Group; import org.exist.security.PermissionDeniedException; import org.exist.security.SecurityManager; import org.exist.security.Session; import org.exist.security.Subject; import org.exist.security.Account; import org.exist.security.Permission; import org.exist.security.Principal; import org.exist.security.SchemaType; import org.exist.security.internal.aider.GroupAider; import org.exist.security.realm.Realm; import org.exist.storage.BrokerPool; import org.exist.storage.BrokerPoolService; import org.exist.storage.BrokerPoolServiceException; import org.exist.storage.DBBroker; import org.exist.storage.txn.TransactionManager; import org.exist.storage.txn.Txn; import org.exist.util.hashtable.Int2ObjectHashMap; import org.exist.xmldb.XmldbURI; import org.quartz.JobDataMap; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.SimpleTrigger; /** * SecurityManager is responsible for managing users and groups. * * There's only one SecurityManager for each database instance, which * may be obtained by {@link BrokerPool#getSecurityManager()}. * * Users and groups are stored in the system collection, in document * users.xml. While it is possible to edit this file by hand, it * may lead to unexpected results, since SecurityManager reads * users.xml only during database startup and shutdown. */ //<!-- Central user configuration. Editing this document will cause the security to reload and update its internal database. Please handle with care! --> @ConfigurationClass("security-manager") public class SecurityManagerImpl implements SecurityManager, BrokerPoolService { public final static int MAX_USER_ID = 1048571; //1 less than RealmImpl.UNKNOWN_ACCOUNT_ID public final static int MAX_GROUP_ID = 1048572; //1 less than RealmImpl.UNKNOWN_GROUP_ID public final static Logger LOG = LogManager.getLogger(SecurityManager.class); private Database db; protected PrincipalDbById<Group> groupsById = new PrincipalDbById<>(); protected PrincipalDbById<Account> usersById = new PrincipalDbById<>(); private final PrincipalLocks<Account> accountLocks = new PrincipalLocks<>(); private final PrincipalLocks<Group> groupLocks = new PrincipalLocks<>(); //TODO: validate & remove if session timeout private SessionDb sessions = new SessionDb(); @ConfigurationFieldAsAttribute("last-account-id") protected int lastUserId = 0; @ConfigurationFieldAsAttribute("last-group-id") protected int lastGroupId = 0; @ConfigurationFieldAsAttribute("version") private String version = "2.0"; @ConfigurationFieldAsElement("authentication-entry-point") public final static String authenticationEntryPoint = "/authentication/login"; private RealmImpl defaultRealm; @ConfigurationFieldAsElement("realm") @ConfigurationFieldClassMask("org.exist.security.realm.%1$s.%2$sRealm") private List<Realm> realms = new ArrayList<>(); @ConfigurationFieldAsElement("events") //@ConfigurationFieldClassMask("org.exist.security.internal.SMEvents") private SMEvents events = null; private Collection collection = null; private Configuration configuration = null; public SecurityManagerImpl(final Database db) { this.db = db; } @Override public void prepare(final BrokerPool brokerPool) throws BrokerPoolServiceException { try { this.defaultRealm = new RealmImpl(null, this, null); realms.add(defaultRealm); } catch(final EXistException e) { throw new BrokerPoolServiceException(e); } } @Override public void startSystem(final DBBroker systemBroker) throws BrokerPoolServiceException { try { attach(systemBroker); } catch(final EXistException e) { throw new BrokerPoolServiceException(e); } } @Override public void startPreMultiUserSystem(final DBBroker systemBroker) throws BrokerPoolServiceException { final Properties params = new Properties(); params.put(getClass().getName(), this); db.getScheduler().createPeriodicJob(TIMEOUT_CHECK_PERIOD, new SessionsCheck(), TIMEOUT_CHECK_PERIOD, params, SimpleTrigger.REPEAT_INDEFINITELY, false); } /** * Initialize the security manager. * * Checks if the file users.xml exists in the system collection of the database. * If not, it is created with two default users: admin and guest. * * @param broker */ @Override public void attach(final DBBroker broker) throws EXistException { //groups = new Int2ObjectHashMap<Group>(65); //users = new Int2ObjectHashMap<User>(65); db = broker.getDatabase(); //TODO: check that db is same? final TransactionManager transaction = db.getTransactionManager(); Collection systemCollection = null; try(final Txn txn = transaction.beginTransaction()) { systemCollection = broker.getCollection(XmldbURI.SYSTEM_COLLECTION_URI); if(systemCollection == null) { systemCollection = broker.getOrCreateCollection(txn, XmldbURI.SYSTEM_COLLECTION_URI); if (systemCollection == null) { return; } systemCollection.setPermissions(Permission.DEFAULT_SYSTEM_COLLECTION_PERM); broker.saveCollection(txn, systemCollection); } transaction.commit(txn); } catch (final Exception e) { e.printStackTrace(); LOG.debug("loading acl failed: " + e.getMessage()); } try(final Txn txn = transaction.beginTransaction()) { collection = broker.getCollection(SECURITY_COLLECTION_URI); if (collection == null) { collection = broker.getOrCreateCollection(txn, SECURITY_COLLECTION_URI); if (collection == null) { return; } //if db corrupted it can lead to unrunnable issue //throw new ConfigurationException("Collection '/db/system/security' can't be created."); collection.setPermissions(Permission.DEFAULT_SYSTEM_SECURITY_COLLECTION_PERM); broker.saveCollection(txn, collection); } transaction.commit(txn); } catch (final Exception e) { e.printStackTrace(); LOG.debug("loading configuration failed: " + e.getMessage()); } final Configuration _config_ = Configurator.parse(this, broker, collection, CONFIG_FILE_URI); configuration = Configurator.configure(this, _config_); for (final Realm realm : realms) { realm.start(broker); } } @Override public boolean updateAccount(final Account account) throws PermissionDeniedException, EXistException { if (account == null) { return false; } if (account.getRealmId() == null) { throw new ConfigurationException("Account must have realm id."); } final Lock lock = accountLocks.getWriteLock(account); lock.lock(); try { return findRealmForRealmId(account.getRealmId()).updateAccount(account); } finally { lock.unlock(); } } @Override public boolean updateGroup(final Group group) throws PermissionDeniedException, EXistException { if (group == null) { return false; } if (group.getRealmId() == null) { throw new ConfigurationException("Group must have realm id."); } final Lock lock = groupLocks.getWriteLock(group); lock.lock(); try { return findRealmForRealmId(group.getRealmId()).updateGroup(group); } finally { lock.unlock(); } } @Override public boolean deleteGroup(final String name) throws PermissionDeniedException, EXistException { final Group group = getGroup(name); if (group == null) { return false; } if (group.getRealmId() == null) { throw new ConfigurationException("Group must have realm id."); } final Lock lock = groupLocks.getWriteLock(group); lock.lock(); try { return findRealmForRealmId(group.getRealmId()).deleteGroup(group); } finally { lock.unlock(); } } @Override public boolean deleteAccount(final String name) throws PermissionDeniedException, EXistException { return deleteAccount(getAccount(name)); } @Override public boolean deleteAccount(final Account account) throws PermissionDeniedException, EXistException { if (account == null) { return false; } if (account.getRealmId() == null) { throw new ConfigurationException("Account must have realm id."); } final Lock lock = accountLocks.getWriteLock(account); lock.lock(); try { return findRealmForRealmId(account.getRealmId()).deleteAccount(account); } finally { lock.unlock(); } } @Override public Account getAccount(final String name) { // if (SYSTEM.equals(name)) { // return defaultRealm.ACCOUNT_SYSTEM; // } for(final Realm realm : realms) { final Account account = realm.getAccount(name); if (account != null) { return account; } } LOG.debug("Account for '" + name + "' not found!"); return null; } @Override public final Account getAccount(final int id) { return usersById.read(principalDb -> principalDb.get(id)); } @Override public boolean hasGroup(final String name) { for (final Realm realm : realms) { if(realm.hasGroup(name)) { return true; } } return false; } @Override public boolean hasGroup(final Group group) { return hasGroup(group.getName()); } @Override public Group getGroup(final String name) { for(final Realm realm : realms) { final Group group = realm.getGroup(name); if(group != null) { return group; } } return null; } @Override public final Group getGroup(final int id) { return groupsById.read(principalDb -> principalDb.get(id)); } @Override public boolean hasAdminPrivileges(final Account user) { final Lock lock = accountLocks.getReadLock(user); lock.lock(); try { return user.hasDbaRole(); } finally { lock.unlock(); } } @Override public boolean hasAccount(final String name) { for(final Realm realm : realms) { if(realm.hasAccount(name)) { return true; } } return false; } @Override public Subject authenticate(final String username, final Object credentials) throws AuthenticationException { if (LOG.isDebugEnabled()) { LOG.debug("Authentication try for '"+username+"'."); } if (username == null) { throw new AuthenticationException( AuthenticationException.ACCOUNT_NOT_FOUND, "Account NULL not found" ); } if("jsessionid".equals(username)) { if (getSystemSubject().getSessionId().equals(credentials)) { return getSystemSubject(); } if (getGuestSubject().getSessionId().equals(credentials)) { return getGuestSubject(); } final Subject subject = sessions.read(db1 -> { final Session session = db1.get(credentials); if (session == null) { return null; } if (session.isValid()) { return session.getSubject(); } return null; }); if(subject == null) { throw new AuthenticationException( AuthenticationException.SESSION_NOT_FOUND, "Session [" + credentials + "] not found" ); } if (events != null) { events.authenticated(subject); } //TODO: validate session return subject; } for(final Realm realm : realms) { try { final Subject subject = realm.authenticate(username, credentials); if (LOG.isDebugEnabled()) { LOG.debug("Authenticated by '" + realm.getId() + "' as '" + subject + "'."); } if (events != null) { events.authenticated(subject); } return subject; } catch(final AuthenticationException e) { if(e.getType() != AuthenticationException.ACCOUNT_NOT_FOUND) { if (LOG.isDebugEnabled()) { LOG.debug("Realm '" + realm.getId() + "' threw exception for account '" + username + "'. [" + e.getMessage() + "]"); } throw e; } } } if (LOG.isDebugEnabled()) { LOG.debug("Account '"+username+"' not found, throw error"); } throw new AuthenticationException( AuthenticationException.ACCOUNT_NOT_FOUND, "Account [" + username + "] not found" ); } protected Subject systemSubject = null; protected Subject guestSubject = null; @Override public Subject getSystemSubject() { if (systemSubject == null) { synchronized (this) { if (systemSubject == null) { systemSubject = new SubjectAccreditedImpl(defaultRealm.ACCOUNT_SYSTEM, this); } } } return systemSubject; } @Override public Subject getGuestSubject() { if (guestSubject == null) { synchronized (this) { if (guestSubject == null) { guestSubject = new SubjectAccreditedImpl((AccountImpl) defaultRealm.getAccount(SecurityManager.GUEST_USER), this); } } } return guestSubject; } @Override public Group getDBAGroup() { return defaultRealm.GROUP_DBA; } @Override public Database getDatabase() { return db; } @Override public Database database() { return db; } private synchronized int getNextGroupId() { if(lastGroupId + 1 == MAX_GROUP_ID) { throw new RuntimeException("System has no more group-ids available"); } return ++lastGroupId; } private synchronized int getNextAccountId() { if(lastUserId +1 == MAX_USER_ID) { throw new RuntimeException("System has no more user-ids available"); } return ++lastUserId; } @Override public List<Account> getGroupMembers(final String groupName) { final List<Account> groupMembers = new ArrayList<>(); for(final Realm realm : realms) { groupMembers.addAll( realm.getAccounts().stream() .filter(account -> account.hasGroup(groupName)) .collect(Collectors.toList()) ); } return groupMembers; } @Override public List<String> findAllGroupMembers(final String groupName) { final List<String> userNames = new ArrayList<>(); for(final Realm realm : realms) { userNames.addAll(realm.findAllGroupMembers(groupName)); } return userNames; } @Deprecated //use realm's getAccounts @Override public java.util.Collection<Account> getUsers() { return defaultRealm.getAccounts(); } @Deprecated //use realm's getGroups @Override public java.util.Collection<Group> getGroups() { return defaultRealm.getGroups(); } @Override public void addGroup(final DBBroker broker, final String name) throws PermissionDeniedException, EXistException { addGroup(broker, new GroupAider(name)); } @Override public Group addGroup(final DBBroker broker, final Group group) throws PermissionDeniedException, EXistException { if(group.getRealmId() == null) { throw new ConfigurationException("Group must have realm id."); } if(group.getName() == null || group.getName().isEmpty()) { throw new ConfigurationException("Group must have name."); } final int id; if(group.getId() != Group.UNDEFINED_ID) { id = group.getId(); } else { id = getNextGroupId(); } final AbstractRealm registeredRealm = (AbstractRealm)findRealmForRealmId(group.getRealmId()); if (registeredRealm.hasGroup(group.getName())) { throw new ConfigurationException("The group '" + group.getName() + "' at realm '" + group.getRealmId() + "' already exist."); } final GroupImpl newGroup = new GroupImpl(broker, registeredRealm, id, group.getName(), group.getManagers()); for(final SchemaType metadataKey : group.getMetadataKeys()) { final String metadataValue = group.getMetadataValue(metadataKey); newGroup.setMetadataValue(metadataKey, metadataValue); } final Lock lock = groupLocks.getWriteLock(newGroup); lock.lock(); try { groupsById.modify(principalDb -> principalDb.put(id, newGroup)); registeredRealm.registerGroup(newGroup); save(broker); newGroup.save(broker); return newGroup; } finally { lock.unlock(); } } @Override public final Account addAccount(final Account account) throws PermissionDeniedException, EXistException { try(final DBBroker broker = db.getBroker()) { return addAccount(broker, account); } } @Override public final Account addAccount(final DBBroker broker, final Account account) throws PermissionDeniedException, EXistException{ if(account.getRealmId() == null) { throw new ConfigurationException("Account must have realm id."); } if(account.getName() == null || account.getName().isEmpty()) { throw new ConfigurationException("Account must have name."); } final int id; if(account.getId() != Account.UNDEFINED_ID) { id = account.getId(); } else { id = getNextAccountId(); } final AbstractRealm registeredRealm = (AbstractRealm) findRealmForRealmId(account.getRealmId()); final AccountImpl newAccount = new AccountImpl(broker, registeredRealm, id, account); final Lock lock = accountLocks.getWriteLock(newAccount); lock.lock(); try { usersById.modify(principalDb -> principalDb.put(id, newAccount)); registeredRealm.registerAccount(newAccount); //XXX: one transaction? save(broker); newAccount.save(broker); return newAccount; } finally { lock.unlock(); } } private void save() throws PermissionDeniedException, EXistException { if (configuration != null) { configuration.save(); } } private void save(final DBBroker broker) throws PermissionDeniedException, EXistException { if (configuration != null) { configuration.save(broker); } } @Override public boolean isConfigured() { return configuration != null; } @Override public Configuration getConfiguration() { return configuration; } //Session management part public final static long TIMEOUT_CHECK_PERIOD = 20000; //20 sec public static class SessionsCheck implements JobDescription, org.quartz.Job { boolean firstRun = true; public SessionsCheck() {} public String getGroup() { return "eXist.Security"; } @Override public String getName() { return "Sessions.Check"; } @Override public void setName(String name) { } @Override public final void execute(final JobExecutionContext jec) throws JobExecutionException { final JobDataMap jobDataMap = jec.getJobDetail().getJobDataMap(); final Properties params = (Properties) jobDataMap.get("params"); if (params == null) { return; } final SecurityManagerImpl sm = (SecurityManagerImpl)params.get(SecurityManagerImpl.class.getName()); if (sm == null) { return; } sm.sessions.modify(db -> { final Iterator<Map.Entry<String, Session>> it = db.entrySet().iterator(); while (it.hasNext()) { final Map.Entry<String, Session> entry = it.next(); if (entry == null || !entry.getValue().isValid()) { it.remove(); } } }); } } @Override public void registerSession(final Session session) { sessions.modify(db -> db.put(session.getId(), session)); } @Override public Subject getSubjectBySessionId(String sessionId) { return sessions.read(db -> { Session session = db.get(sessionId); if (session != null) { return session.getSubject(); } return null; }); } private Realm findRealmForRealmId(final String realmId) throws ConfigurationException { for(final Realm realm : realms) { if(realm.getId().equals(realmId)) { return realm; } } throw new ConfigurationException("Realm id = '" + realmId + "' not found."); } @Override public void addGroup(final int id, final Group group) { groupsById.modify(principalDb -> principalDb.put(id, group)); } @Override public void addUser(final int id, final Account account) { usersById.modify(principalDb -> principalDb.put(id, account)); } @Override public boolean hasGroup(final int id) { return groupsById.read(principalDb -> principalDb.containsKey(id)); } @Override public boolean hasUser(final int id) { return usersById.read(principalDb -> principalDb.containsKey(id)); } @Override public List<String> findUsernamesWhereNameStarts(final String startsWith) { final List<String> userNames = new ArrayList<>(); for(final Realm realm : realms) { userNames.addAll(realm.findUsernamesWhereNameStarts(startsWith)); } return userNames; } @Override public List<String> findUsernamesWhereUsernameStarts(final String startsWith) { final List<String> userNames = new ArrayList<>(); for(final Realm realm : realms) { userNames.addAll(realm.findUsernamesWhereUsernameStarts(startsWith)); } return userNames; } @Override public List<String> findUsernamesWhereNamePartStarts(final String startsWith) { final List<String> userNames = new ArrayList<>(); for(final Realm realm : realms) { userNames.addAll(realm.findUsernamesWhereNamePartStarts(startsWith)); } return userNames; } @Override public List<String> findGroupnamesWhereGroupnameContains(final String fragment) { final List<String> groupNames = new ArrayList<>(); for(final Realm realm : realms) { groupNames.addAll(realm.findGroupnamesWhereGroupnameContains(fragment)); } return groupNames; } @Override public List<String> findGroupnamesWhereGroupnameStarts(final String startsWith) { final List<String> groupNames = new ArrayList<>(); for(final Realm realm : realms) { groupNames.addAll(realm.findGroupnamesWhereGroupnameStarts(startsWith)); } return groupNames; } @Override public List<String> findAllGroupNames() { final List<String> groupNames = new ArrayList<>(); for(final Realm realm : realms) { groupNames.addAll(realm.findAllGroupNames()); } return groupNames; } @Override public List<String> findAllUserNames() { final List<String> userNames = new ArrayList<>(); for(final Realm realm : realms) { userNames.addAll(realm.findAllUserNames()); } return userNames; } private Map<XmldbURI, Integer> saving = new HashMap<>(); @Override public void processPramatterBeforeSave(final DBBroker broker, final DocumentImpl document) throws ConfigurationException { XmldbURI uri = document.getCollection().getURI(); final boolean isRemoved = uri.endsWith(SecurityManager.REMOVED_COLLECTION_URI); if(isRemoved) { uri = uri.removeLastSegment(); } final boolean isAccount = uri.endsWith(SecurityManager.ACCOUNTS_COLLECTION_URI); final boolean isGroup = uri.endsWith(SecurityManager.GROUPS_COLLECTION_URI); if(isAccount || isGroup) { //uri = uri.removeLastSegment(); //String realmId = uri.lastSegment().toString(); //AbstractRealm realm = (AbstractRealm)findRealmForRealmId(realmId); final Configuration conf = Configurator.parse(broker.getBrokerPool(), document); saving.put(document.getURI(), conf.getPropertyInteger("id")); } } @Override public void processPramatter(DBBroker broker, DocumentImpl document) throws ConfigurationException { XmldbURI uri = document.getCollection().getURI(); //System.out.println(document); final boolean isRemoved = uri.endsWith(SecurityManager.REMOVED_COLLECTION_URI); if(isRemoved) { uri = uri.removeLastSegment(); } final boolean isAccount = uri.endsWith(SecurityManager.ACCOUNTS_COLLECTION_URI); final boolean isGroup = uri.endsWith(SecurityManager.GROUPS_COLLECTION_URI); if(isAccount || isGroup) { uri = uri.removeLastSegment(); final String realmId = uri.lastSegment().toString(); final AbstractRealm realm = (AbstractRealm)findRealmForRealmId(realmId); final Configuration conf = Configurator.parse(broker.getBrokerPool(), document); Integer id = -1; if(isRemoved) { id = conf.getPropertyInteger("id"); } final String name = conf.getProperty("name"); if(isAccount) { if (isRemoved && id > 2 && !hasUser(id)) { final AccountImpl account = new AccountImpl( realm, conf ); account.removed = true; addUser(account.getId(), account); } else if(name != null) { if (realm.hasAccount(name)) { final Integer oldId = saving.get(document.getURI()); final Integer newId = conf.getPropertyInteger("id"); //XXX: resolve conflicts on ids!!! if (!newId.equals(oldId)) { final Account current = realm.getAccount(name); accountLocks.getWriteLock(current).lock(); try { usersById.modify(principalDb -> { principalDb.remove(oldId); principalDb.put(newId, current); }); } finally { accountLocks.getWriteLock(current).unlock(); } } } else { final Account account = new AccountImpl( realm, conf ); addUser(account.getId(), account); realm.registerAccount(account); } } else { //this can't be! log any way LOG.error("Account '"+name+"' pressent at '"+realmId+"' realm, but get event that new one created."); } } else if(isGroup) { if (isRemoved && id > 2 && !hasGroup(id)) { final GroupImpl group = new GroupImpl( realm, conf ); group.removed = true; addGroup(group.getId(), group); } else if (name != null && !realm.hasGroup(name)) { final GroupImpl group = new GroupImpl( realm, conf ); addGroup(group.getId(), group); realm.registerGroup(group); } else { //this can't be! log any way LOG.error("Group '"+name+"' pressent at '"+realmId+"' realm, but get event that new one created."); } } saving.remove(document.getURI()); } } @Override public String getAuthenticationEntryPoint() { return authenticationEntryPoint; } private static class PrincipalLocks<T extends Principal> { private final Map<Integer, ReentrantReadWriteLock> locks = new HashMap<>(); private synchronized ReentrantReadWriteLock getLock(final T principal) { ReentrantReadWriteLock lock = locks.get(principal.getId()); if(lock == null) { lock = new ReentrantReadWriteLock(); locks.put(principal.getId(), lock); } return lock; } public ReadLock getReadLock(T principal) { return getLock(principal).readLock(); } public WriteLock getWriteLock(T principal) { return getLock(principal).writeLock(); } } protected static class SessionDb { private final Map<String, Session> db = new HashMap<>(); private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private final ReadLock readLock = lock.readLock(); private final WriteLock writeLock = lock.writeLock(); public <R> R read(final Function<Map<String, Session>, R> readFn) { readLock.lock(); try { return readFn.apply(db); } finally { readLock.unlock(); } } public final void modify(final Consumer<Map<String, Session>> modifyFn) { writeLock.lock(); try { modifyFn.accept(db); } finally { writeLock.unlock(); } } } protected static class PrincipalDbById<V extends Principal> { private final Int2ObjectHashMap<V> db = new Int2ObjectHashMap<>(65); private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private final ReadLock readLock = lock.readLock(); private final WriteLock writeLock = lock.writeLock(); public <R> R read(final Function<Int2ObjectHashMap<V>, R> readFn) { readLock.lock(); try { return readFn.apply(db); } finally { readLock.unlock(); } } public final void modify(final Consumer<Int2ObjectHashMap<V>> writeOp) { writeLock.lock(); try { writeOp.accept(db); } finally { writeLock.unlock(); } } } @Override public Subject getCurrentSubject() { return db.getActiveBroker().getCurrentSubject(); } @Override public final synchronized void preAllocateAccountId(final PrincipalIdReceiver receiver) throws PermissionDeniedException, EXistException { final int id = getNextAccountId(); save(); receiver.allocate(id); } @Override public final synchronized void preAllocateGroupId(final PrincipalIdReceiver receiver) throws PermissionDeniedException, EXistException { final int id = getNextGroupId(); save(); receiver.allocate(id); } }