/* * Copyright (C) 2005-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.openfire.ldap; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import java.util.regex.Matcher; import javax.naming.directory.Attributes; import javax.naming.directory.DirContext; import org.dom4j.Document; import org.dom4j.DocumentHelper; import org.dom4j.Element; import org.dom4j.Node; import org.jivesoftware.openfire.vcard.DefaultVCardProvider; import org.jivesoftware.openfire.vcard.VCardManager; import org.jivesoftware.openfire.vcard.VCardProvider; import org.jivesoftware.util.AlreadyExistsException; import org.jivesoftware.util.Base64; import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.NotFoundException; import org.jivesoftware.util.PropertyEventDispatcher; import org.jivesoftware.util.PropertyEventListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xmpp.packet.JID; /** * Read-only LDAP provider for vCards.Configuration consists of adding a provider: * <p> * <tt>provider.vcard.className = org.jivesoftware.openfire.ldap.LdapVCardProvider</tt> * </p> * <p>and an xml vcard-mapping in the system properties.</p> * <p> * The vcard attributes can be configured by adding an <code>attrs="attr1,attr2"</code> * attribute to the vcard elements.</p> * <p> * Arbitrary text can be used for the element values as well as <code>MessageFormat</code> * style placeholders for the ldap attributes. For example, if you wanted to map the LDAP * attribute <code>displayName</code> to the vcard element <code>FN</code>, the xml * nippet would be:</p><br><pre><FN attrs="displayName">{0}</FN></pre> * <p> * The vCard XML must be escaped in CDATA and must also be well formed. It is the exact * XML this provider will send to a client after after stripping <code>attr</code> attributes * and populating the placeholders with the data retrieved from LDAP. This system should * be flexible enough to handle any client's vCard format. An example mapping follows.<br> * </p> * <tt>ldap.vcard-mapping = * <![CDATA[ * <vCard xmlns='vcard-temp'> * <FN attrs="displayName">{0}</FN> * <NICKNAME attrs="uid">{0}</NICKNAME> * <BDAY attrs="dob">{0}</BDAY> * <ADR> * <HOME/> * <EXTADR>Ste 500</EXTADR> * <STREET>317 SW Alder St</STREET> * <LOCALITY>Portland</LOCALITY> * <REGION>Oregon</REGION> * <PCODE>97204</PCODE> * <CTRY>USA</CTRY> * </ADR> * <TEL> * <HOME/> * <VOICE/> * <NUMBER attrs="telephoneNumber">{0}</NUMBER> * </TEL> * <EMAIL> * <INTERNET/> * <USERID attrs="mail">{0}</USERID> * </EMAIL> * <TITLE attrs="title">{0}</TITLE> * <ROLE attrs="">{0}</ROLE> * <ORG> * <ORGNAME attrs="o">{0}</ORGNAME> * <ORGUNIT attrs="">{0}</ORGUNIT> * </ORG> * <URL attrs="labeledURI">{0}</URL> * <DESC attrs="uidNumber,homeDirectory,loginShell"> * uid: {0} home: {1} shell: {2} * </DESC> * </vCard> * ]]> * </tt> * <p> * An easy way to get the vcard format your client needs, assuming you've been * using the database store, is to do a <code>SELECT value FROM ofVCard WHERE * username='some_user'</code> in your favorite sql querier and paste the result * into the <code>vcard-mapping</code> (don't forget the CDATA).</p> * * @author rkelly */ public class LdapVCardProvider implements VCardProvider, PropertyEventListener { private static final Logger Log = LoggerFactory.getLogger(LdapVCardProvider.class); private LdapManager manager; private VCardTemplate template; private Boolean dbStorageEnabled = false; /** * The default vCard provider is used to handle the vCard in the database. vCard * fields that can be overriden are stored in the database. * * This is used/created only if we are storing avatars in the database. */ private DefaultVCardProvider defaultProvider = null; public LdapVCardProvider() { // Convert XML based provider setup to Database based JiveGlobals.migrateProperty("ldap.vcard-mapping"); manager = LdapManager.getInstance(); initTemplate(); // Listen to property events so that the template is always up to date PropertyEventDispatcher.addListener(this); // DB vcard provider used for loading properties overwritten in the DB defaultProvider = new DefaultVCardProvider(); // Check of avatars can be overwritten (and stored in the database) dbStorageEnabled = JiveGlobals.getBooleanProperty("ldap.override.avatar", false); } /** * Initializes the VCard template as set by the administrator. */ private void initTemplate() { String property = JiveGlobals.getProperty("ldap.vcard-mapping"); Log.debug("LdapVCardProvider: Found vcard mapping: '" + property); try { // Remove CDATA wrapping element if (property.startsWith("<![CDATA[")) { property = property.substring(9, property.length()-3); } Document document = DocumentHelper.parseText(property); template = new VCardTemplate(document); } catch (Exception e) { Log.error("Error loading vcard mapping: " + e.getMessage()); } Log.debug("LdapVCardProvider: attributes size==" + template.getAttributes().length); } /** * Creates a mapping of requested LDAP attributes to their values for the given user. * * @param username User we are looking up in LDAP. * @return Map of LDAP attribute to setting. */ private Map<String, String> getLdapAttributes(String username) { // Un-escape username username = JID.unescapeNode(username); Map<String, String> map = new HashMap<>(); DirContext ctx = null; try { String userDN = manager.findUserDN(username); ctx = manager.getContext(manager.getUsersBaseDN(username)); Attributes attrs = ctx.getAttributes(userDN, template.getAttributes()); for (String attribute : template.getAttributes()) { javax.naming.directory.Attribute attr = attrs.get(attribute); String value; if (attr == null) { Log.debug("LdapVCardProvider: No ldap value found for attribute '" + attribute + "'"); value = ""; } else { Object ob = attrs.get(attribute).get(); Log.debug("LdapVCardProvider: Found attribute "+attribute+" of type: "+ob.getClass()); if(ob instanceof String) { value = (String)ob; } else { value = Base64.encodeBytes((byte[])ob); } } Log.debug("LdapVCardProvider: Ldap attribute '" + attribute + "'=>'" + value + "'"); map.put(attribute, value); } return map; } catch (Exception e) { Log.error(e.getMessage(), e); return Collections.emptyMap(); } finally { try { if (ctx != null) { ctx.close(); } } catch (Exception e) { // Ignore. } } } /** * Loads the avatar from LDAP, based off the vcard template. * * If enabled, will replace a blank PHOTO element with one from a DB stored vcard. * * @param username User we are loading the vcard for. * @return The loaded vcard element, or null if none found. */ @Override public Element loadVCard(String username) { // Un-escape username. username = JID.unescapeNode(username); Map<String, String> map = getLdapAttributes(username); Log.debug("LdapVCardProvider: Getting mapped vcard for " + username); Element vcard = new VCard(template).getVCard(map); // If we have a vcard from ldap, but it doesn't have an avatar filled in, then we // may fill it with a locally stored vcard element. if (dbStorageEnabled && vcard != null && (vcard.element("PHOTO") == null || vcard.element("PHOTO").element("BINVAL") == null || vcard.element("PHOTO").element("BINVAL").getText().matches("\\s*"))) { Element avatarElement = loadAvatarFromDatabase(username); if (avatarElement != null) { Log.debug("LdapVCardProvider: Adding avatar element from local storage"); Element currentElement = vcard.element("PHOTO"); if (currentElement != null) { vcard.remove(currentElement); } vcard.add(avatarElement); } } Log.debug("LdapVCardProvider: Returning vcard"); return vcard; } /** * Returns a merged LDAP vCard combined with a PHOTO element provided in specified vCard. * * @param username User whose vCard this is. * @param mergeVCard vCard element that we are merging PHOTO element from into the LDAP vCard. * @return vCard element after merging in PHOTO element to LDAP data. */ private Element getMergedVCard(String username, Element mergeVCard) { // Un-escape username. username = JID.unescapeNode(username); Map<String, String> map = getLdapAttributes(username); Log.debug("LdapVCardProvider: Retrieving LDAP mapped vcard for " + username); if (map.isEmpty()) { return null; } Element vcard = new VCard(template).getVCard(map); if (mergeVCard == null) { // No vcard passed in? Hrm. Fine, return LDAP vcard. return vcard; } if (mergeVCard.element("PHOTO") == null) { // Merged vcard has no photo element, return LDAP vcard as is. return vcard; } Element photoElement = mergeVCard.element("PHOTO").createCopy(); if (photoElement == null || photoElement.element("BINVAL") == null || photoElement.element("BINVAL").getText().matches("\\s*")) { // We were passed something null or empty, so lets just return the LDAP based vcard. return vcard; } // Now we need to check that the LDAP vcard doesn't have a PHOTO element that's filled in. if (!((vcard.element("PHOTO") == null || vcard.element("PHOTO").element("BINVAL") == null || vcard.element("PHOTO").element("BINVAL").getText().matches("\\s*")))) { // Hrm, it does, return the original vcard; return vcard; } Log.debug("LdapVCardProvider: Merging avatar element from passed vcard"); Element currentElement = vcard.element("PHOTO"); if (currentElement != null) { vcard.remove(currentElement); } vcard.add(photoElement); return vcard; } /** * Loads the avatar element from the user's DB stored vcard. * * @param username User whose vcard/avatar element we are loading. * @return Loaded avatar element or null if not found. */ private Element loadAvatarFromDatabase(String username) { Element vcardElement = defaultProvider.loadVCard(username); Element avatarElement = null; if (vcardElement != null && vcardElement.element("PHOTO") != null) { avatarElement = vcardElement.element("PHOTO").createCopy(); } return avatarElement; } /** * Handles when a user creates a new vcard. * * @param username User that created a new vcard. * @param vCardElement vCard element containing the new vcard. * @throws UnsupportedOperationException If an invalid field is changed or we are in readonly mode. */ @Override public Element createVCard(String username, Element vCardElement) throws UnsupportedOperationException, AlreadyExistsException { throw new UnsupportedOperationException("LdapVCardProvider: VCard changes not allowed."); } /** * Handles when a user updates their vcard. * * @param username User that updated their vcard. * @param vCardElement vCard element containing the new vcard. * @throws UnsupportedOperationException If an invalid field is changed or we are in readonly mode. */ @Override public Element updateVCard(String username, Element vCardElement) throws UnsupportedOperationException { if (dbStorageEnabled && defaultProvider != null) { if (isValidVCardChange(username, vCardElement)) { Element mergedVCard = getMergedVCard(username, vCardElement); try { defaultProvider.updateVCard(username, mergedVCard); } catch (NotFoundException e) { try { defaultProvider.createVCard(username, mergedVCard); } catch (AlreadyExistsException e1) { // Ignore } } return mergedVCard; } else { throw new UnsupportedOperationException("LdapVCardProvider: Invalid vcard changes."); } } else { throw new UnsupportedOperationException("LdapVCardProvider: VCard changes not allowed."); } } /** * Handles when a user deletes their vcard. * * @param username User that deketed their vcard. * @throws UnsupportedOperationException If an invalid field is changed or we are in readonly mode. */ @Override public void deleteVCard(String username) throws UnsupportedOperationException { throw new UnsupportedOperationException("LdapVCardProvider: Attempted to delete vcard in read-only mode."); } /** * Returns true or false if the change to the existing vcard is valid (only to PHOTO element) * * @param username User who's LDAP-based vcard we will compare with. * @param newvCard New vCard Element we will compare against. * @return True or false if the changes made were valid (only to PHOTO element) */ private Boolean isValidVCardChange(String username, Element newvCard) { if (newvCard == null) { // Well if there's nothing to change, of course it's valid. Log.debug("LdapVCardProvider: No new vcard provided (no changes), accepting."); return true; } // Un-escape username. username = JID.unescapeNode(username); Map<String, String> map = getLdapAttributes(username); // Retrieve LDAP created vcard for comparison Element ldapvCard = new VCard(template).getVCard(map); if (ldapvCard == null) { // This person has no vcard at all, may not change it! Log.debug("LdapVCardProvider: User has no LDAP vcard, nothing they can change, rejecting."); return false; } // If the LDAP vcard has a non-empty PHOTO element set, then there is literally no way this will be accepted. Element ldapPhotoElem = ldapvCard.element("PHOTO"); if (ldapPhotoElem != null) { Element ldapBinvalElem = ldapPhotoElem.element("BINVAL"); if (ldapBinvalElem != null && !ldapBinvalElem.getTextTrim().matches("\\s*")) { // LDAP is providing a valid PHOTO element, byebye! Log.debug("LdapVCardProvider: LDAP has a PHOTO element set, no way to override, rejecting."); return false; } } // Retrieve database vcard, if it exists Element dbvCard = defaultProvider.loadVCard(username); if (dbvCard != null) { Element dbPhotoElem = dbvCard.element("PHOTO"); if (dbPhotoElem == null) { // DB has no photo, lets accept what we got. Log.debug("LdapVCardProvider: Database has no PHOTO element, accepting update."); return true; } else { Element newPhotoElem = newvCard.element("PHOTO"); if (newPhotoElem == null) { Log.debug("LdapVCardProvider: Photo element was removed, accepting update."); return true; } // Note: NodeComparator never seems to consider these equal, even if they are? if (!dbPhotoElem.asXML().equals(newPhotoElem.asXML())) { // Photo element was changed. Ignore all other changes and accept this. Log.debug("LdapVCardProvider: PHOTO element changed, accepting update."); return true; } } } else { // No vcard exists in database Log.debug("LdapVCardProvider: Database has no vCard stored, accepting update."); return true; } // Ok, either something bad changed or nothing changed. Either way, user either: // 1. should not have tried to change something 'readonly' // 2. shouldn't have bothered submitting no changes // So we'll consider this a bad return. Log.debug("LdapVCardProvider: PHOTO element didn't change, no reason to accept this, rejecting."); return false; } @Override public boolean isReadOnly() { return !dbStorageEnabled; } @Override public void propertySet(String property, Map params) { if ("ldap.override.avatar".equals(property)) { dbStorageEnabled = Boolean.parseBoolean((String)params.get("value")); } else if ("ldap.vcard-mapping".equals(property)) { initTemplate(); // Reset cache of vCards VCardManager.getInstance().reset(); } } @Override public void propertyDeleted(String property, Map params) { if ("ldap.override.avatar".equals(property)) { dbStorageEnabled = false; } } @Override public void xmlPropertySet(String property, Map params) { //Ignore } @Override public void xmlPropertyDeleted(String property, Map params) { //Ignore } /** * Class to hold a <code>Document</code> representation of a vcard mapping * and unique attribute placeholders. Used by <code>VCard</code> to apply * a <code>Map</code> of ldap attributes to ldap values via * <code>MessageFormat</code> * * @author rkelly */ private static class VCardTemplate { private Document document; private String[] attributes; public VCardTemplate(Document document) { Set<String> set = new HashSet<>(); this.document = document; treeWalk(this.document.getRootElement(), set); attributes = set.toArray(new String[set.size()]); } public String[] getAttributes() { return attributes; } public Document getDocument() { return document; } private void treeWalk(Element element, Set<String> set) { for (int i = 0, size = element.nodeCount(); i < size; i++) { Node node = element.node(i); if (node instanceof Element) { Element emement = (Element) node; StringTokenizer st = new StringTokenizer(emement.getTextTrim(), ", //{}"); while (st.hasMoreTokens()) { // Remove enclosing {} String string = st.nextToken().replaceAll("(\\{)([\\d\\D&&[^}]]+)(})", "$2"); Log.debug("VCardTemplate: found attribute " + string); set.add(string); } treeWalk(emement, set); } } } } /** * vCard class that converts vcard data using a template. */ private static class VCard { private VCardTemplate template; public VCard(VCardTemplate template) { this.template = template; } public Element getVCard(Map<String, String> map) { Document document = (Document) template.getDocument().clone(); Element element = document.getRootElement(); return treeWalk(element, map); } private Element treeWalk(Element element, Map<String, String> map) { for (int i = 0, size = element.nodeCount(); i < size; i++) { Node node = element.node(i); if (node instanceof Element) { Element emement = (Element) node; String elementText = emement.getTextTrim(); if (elementText != null && !"".equals(elementText)) { String format = emement.getStringValue(); StringTokenizer st = new StringTokenizer(elementText, ", //{}"); while (st.hasMoreTokens()) { // Remove enclosing {} String field = st.nextToken(); String attrib = field.replaceAll("(\\{)(" + field + ")(})", "$2"); String value = map.get(attrib); format = format.replaceFirst("(\\{)(" + field + ")(})", Matcher.quoteReplacement(value)); } emement.setText(format); } treeWalk(emement, map); } } return element; } } }