Openfire not syncing with LDAP

Hallo,

I opened a ticket to the community discussion, but I 'd like to be a bit more specific here.

Setup:

  • Openfire 4.0.3 (but previous versions have it, too)
  • Embedded database
  • Authentication using LDAP (Active Directory or OpenLDAP)
    When an admin LDAP users is added to/removed from LDAP, the modification is not propagated to Openfire; one needs to restart Openfire for the changes to be viewed.

Possible cause, the embedded database(?). We changed to an Oracle instance, thus the LDAP admins are stored in admin.authorizedJIDs entry in OFPROPERTY table. In order to simulate a modification in LDAP, we manually modified the values of admin.authorizedJIDs entry in the database hoping that Openfire would pick them up and update the relevant Openfire web admin page, but that was not the case.

Digging inside the Openfire code:

Class org.jivesoftware.openfire.admin.DefaultAdminProvider contains a property change listener that refreshes the admin accounts:

// Detect when the list of admin users is changed.
PropertyEventListener propListener = new PropertyEventListener() {
    @Override
     public void propertySet(String property, Map params) {
                if ("admin.authorizedJIDs".equals(property)) {
                    AdminManager.getInstance().refreshAdminAccounts();
                }
            }
            @Override
            public void propertyDeleted(String property, Map params) {
                if ("admin.authorizedJIDs".equals(property)) {
                    AdminManager.getInstance().refreshAdminAccounts();
                }
            }
            @Override
            public void xmlPropertySet(String property, Map params) {
                //Ignore
            }
            @Override
            public void xmlPropertyDeleted(String property, Map params) {
                //Ignore
            }         };      PropertyEventDispatcher.addListener(propListener);

In AdminManager:

/** * Reads the admin list from the provider and sets up the cache. */ private void loadAdminList() { adminList = provider.getAdmins(); } /** * Refreshs the list of admin users from the provider. */ public void refreshAdminAccounts() { loadAdminList(); }

which returns back to DefaultAdminProvider:

/** * The default provider retrieves the comma separated list from the system property * <tt>admin.authorizedJIDs</tt> * @see org.jivesoftware.openfire.admin.AdminProvider#getAdmins() */ @Override public List<JID> getAdmins() { List<JID> adminList = new ArrayList<>(); // Add bare JIDs of users that are admins (may include remote users), primarily used to override/add to list of admin users String jids = JiveGlobals.getProperty("admin.authorizedJIDs"); jids = (jids == null || jids.trim().length() == 0) ? "" : jids; StringTokenizer tokenizer = new StringTokenizer(jids, ","); while (tokenizer.hasMoreTokens()) { String jid = tokenizer.nextToken().toLowerCase().trim(); try { adminList.add(new JID(jid)); } catch (IllegalArgumentException e) { Log.warn("Invalid JID found in admin.authorizedJIDs system property: " + jid, e); } } if (adminList.isEmpty()) { // Add default admin account when none was specified adminList.add(new JID("admin", XMPPServer.getInstance().getServerInfo().getXMPPDomain(), null, true)); } return adminList; }

So, the question is why it doesn’t check the database?

setup-admin-settings.jsp is responsible for rendering the relevant web page.

// This handles the case of reverting back to default settings from LDAP. Will // add admin to the authorizedJIDs list if the authorizedJIDs list contains // entries. if (!ldap && !doTest) { String currentAdminList = xmppSettings.get("admin.authorizedJIDs"); List<String> adminCollection = new ArrayList<String>(StringUtils.stringToCollection(currentAdminList)); if ((!adminCollection.isEmpty() && !adminCollection.contains("admin")) || xmppSettings.get("admin.authorizedJIDs") != null) { adminCollection.add(new JID("admin", domain, null).toBareJID()); xmppSettings.put("admin.authorizedJIDs", StringUtils.collectionToString(adminCollection)); } } // Save the updated settings session.setAttribute("xmppSettings", xmppSettings);

So the settings that the previous java class parses are the ones saved by the .jsp page to the system property admin.authorizedJIDs.

if (addAdmin && !doTest) { final String admin = request.getParameter("administrator"); if (admin != null) { if (ldap) { // Try to verify that the username exists in LDAP Map<String, String> settings = (Map<String, String>) session.getAttribute("ldapSettings"); Map<String, String> userSettings = (Map<String, String>) session.getAttribute("ldapUserSettings"); if (settings != null) { LdapManager manager = new LdapManager(settings); manager.setUsernameField(userSettings.get("ldap.usernameField")); manager.setSearchFilter(userSettings.get("ldap.searchFilter")); try { manager.findUserDN(JID.unescapeNode(admin)); } catch (Exception e) { e.printStackTrace(); errors.put("administrator", ""); } } } if (errors.isEmpty()) { String currentList = xmppSettings.get("admin.authorizedJIDs"); final List users = new ArrayList(StringUtils.stringToCollection(currentList)); users.add(new JID(admin.toLowerCase(), domain, null).toBareJID()); String userList = StringUtils.collectionToString(users); xmppSettings.put("admin.authorizedJIDs", userList); } } else { errors.put("administrator", ""); } }

I guess that the above is also an action from the Openfire GUI, in order not to add a user to LDAP twice.

Same case for delete admins:

if (deleteAdmins) { String[] params = request.getParameterValues("remove"); String currentAdminList = xmppSettings.get("admin.authorizedJIDs"); Collection<String> adminCollection = StringUtils.stringToCollection(currentAdminList); List temporaryUserList = new ArrayList<String>(adminCollection); final int no = params != null ? params.length : 0; for (int i = 0; i < no; i++) { temporaryUserList.remove(params[i]); } String newUserList = StringUtils.collectionToString(temporaryUserList); if (temporaryUserList.size() == 0) { xmppSettings.put("admin.authorizedJIDs", ""); } else { xmppSettings.put("admin.authorizedJIDs", newUserList); } }

org.jivesoftware.util.JiveProperties class is a map of properties:

private static final String LOAD_PROPERTIES = “SELECT name, propValue FROM ofProperty”;

private static final String INSERT_PROPERTY = “INSERT INTO ofProperty(name, propValue) VALUES(?,?)”;

private static final String UPDATE_PROPERTY = “UPDATE ofProperty SET propValue=? WHERE name=?”;

private static final String DELETE_PROPERTY = “DELETE FROM ofProperty WHERE name LIKE ?”;

which is used by JiveGlobals which is used by DefaultAdminProvider above.

So this system property should be retrieved from the database. Then why is the propListener not been called in the first place?

Is it something missing in my rationale?

Thank you in advance for your replies.

Since I got no reply, after some investigation I found the following solution which works for Oracle DB only, I 'm afraid. The JDBC driver needs to support this, and ojdbc6.jar needs to be in the classpath (i.e. lib directory of openfire). The following class can be called from the constructor of AdminManager as OracleDCN.getInstance()

(you may find the file also attached as there are problems with formatting when attached inline):

/** * Copyright (C) 2004-2008 Jive Software. All rights reserved. * * 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.jivesoftware.database; import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;
import oracle.jdbc.OracleConnection;
import oracle.jdbc.OracleStatement;
import oracle.jdbc.dcn.DatabaseChangeEvent;
import oracle.jdbc.dcn.DatabaseChangeListener;
import oracle.jdbc.dcn.DatabaseChangeRegistration;
import oracle.jdbc.dcn.RowChangeDescription;
import oracle.jdbc.dcn.RowChangeDescription.RowOperation;
import oracle.jdbc.dcn.TableChangeDescription;
import org.jivesoftware.database.DbConnectionManager.DatabaseType;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.XMPPServerListener;
import org.jivesoftware.util.JiveGlobals;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; /** * Oracle Database Change Notification (DCN). Listens for changes in * {@code OFPROPERTY.admin.authorizedJIDs}. Works from Oracle 11gR1 and requires * {@code ojdbc6.jar} in the classpath. * * @author ikost */
public class OracleDCN {     private static final Logger Log = LoggerFactory.getLogger(OracleDCN.class);    private static final class SingletonHolder {         private SingletonHolder() {
        }
        private static final OracleDCN INSTANCE = new OracleDCN();     }     private OracleDCN() {
        try {
            registerOracleDCN();
        } catch (SQLException ex) {
            Log.error("Error creating instance of " + OracleDCN.class.getName(), ex);
        }
    }     /**
     * Only one listener is enough.
     *
     * @return the singleton instance
     * @throws java.sql.SQLException
     */
    public static OracleDCN getInstance() throws SQLException {
        return SingletonHolder.INSTANCE;
    }     /**
     * @see
     * <a href="http://docs.oracle.com/cd/E11882_02/java.112/e16548/dbchgnf.htm#JJDBC28815">link</a>
     * @throws SQLException
     */
    private void registerOracleDCN() throws SQLException {
        if (DbConnectionManager.getDatabaseType().equals(DatabaseType.oracle)) {
            final OracleConnection connection = (OracleConnection) DbConnectionManager.getConnection();
            if (connection != null) {
                Properties props = new Properties();
                props.put(OracleConnection.DCN_NOTIFY_ROWIDS, "true");
                DatabaseChangeRegistration dcr1 = null;
                try {
                    final DatabaseChangeRegistration dcr = connection.registerDatabaseChangeNotification(props);
                    dcr1 = dcr;
                    XMPPServer.getInstance().addServerListener(new XMPPServerListener() {                         @Override
                        public void serverStarted() {
                        }                         @Override
                        public void serverStopping() {
                            if (connection != null) {
                                try {
                                    connection.unregisterDatabaseChangeNotification(dcr);
                                    connection.close();
                                } catch (SQLException ex) {
                                    Log.error("Failed to unregister the Database Change Notification in " + OracleDCN.class.getName(), ex);
                                }
                            }
                        }
                    });
                    dcr.addListener(new DatabaseChangeListener() {                         @Override
                        public void onDatabaseChangeNotification(DatabaseChangeEvent dce) {
                            if (dce.getRegId() == dcr.getRegId()) {  // suppress duplicate events
                                TableChangeDescription[] tcds = dce.getTableChangeDescription();
                                for (TableChangeDescription tcd : tcds) {
                                    if (tcd.getTableName().equals("CHATUSER.OFPROPERTY")) {
                                        RowChangeDescription[] rcds = tcd.getRowChangeDescription();
                                        for (RowChangeDescription rcd : rcds) {
                                            if (rcd.getRowOperation().equals(RowOperation.UPDATE)) {
                                                new Thread(new Runnable() {                                                     @Override
                                                    public void run() {
                                                        try {
                                                            JiveGlobals.setProperty("admin.authorizedJIDs", getAdminAuthorizedJIDs(connection, dcr));
                                                        } catch (SQLException ex) {
                                                            Log.error("Error setting admin.authorizedJIDs property in " + OracleDCN.class.getName(), ex);
                                                        }
                                                        // AdminManager.getInstance().refreshAdminAccounts(); is called automatically when the admin.authorizedJIDs property is modified
                                                    }
                                                }).run();
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    });
                    getAdminAuthorizedJIDs(connection, dcr);
                } catch (SQLException ex) {
                    Log.error("Exception in " + OracleDCN.class.getName(), ex);
                    if (connection != null) {
                        connection.unregisterDatabaseChangeNotification(dcr1);
                        connection.close();
                    }
                } finally {
                    // do not close the connection for the listener to keep working
//            if (connection != null) {
//                connection.unregisterDatabaseChangeNotification(dcr);
//                connection.close();
//            }
                }
            }
        }
    }     /**
     * Retrieves the new admin.authorizedJIDs from the database.
     *
     * @param connection SQL connection to the Oracle instance
     * @param dcr the database change notification registration
     * @return a concatenated string of admin.authorizedJIDs
     * @throws SQLException in case something goes wrong with the connection or
     * the registration
     */
    private String getAdminAuthorizedJIDs(OracleConnection connection, DatabaseChangeRegistration dcr) throws SQLException {
        Statement stmt = connection.createStatement();
        ((OracleStatement) stmt).setDatabaseChangeRegistration(dcr);
        ResultSet rs = stmt.executeQuery("SELECT propvalue FROM ofproperty WHERE name='admin.authorizedJIDs'");
        String adminAuthorizedJIDs = "";
        while (rs.next()) {
            adminAuthorizedJIDs = rs.getString(1);
        }
        rs.close();
        stmt.close();
        return adminAuthorizedJIDs;
    }
}

Your Oracle user needs to be granted the

CHANGE NOTIFICATION

privilege for the above to work.

The only remaining issue is that the Openfire’s admin page is not refreshed automatically when this class is called. The HTTP protocol is not designed to automatically push data to the browser. Possible solutions may be found here.

I tried RowSetProvider (Java Platform SE 7 ) but it didn’t work for me neither with Oracle DB nor with HSQLDB.
OracleDCN.java.zip (2236 Bytes)