View Javadoc

1   /**
2    * Copyright 2009 OPS4J
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.ops4j.pax.useradmin.provider.ldap.internal;
19  
20  import java.io.UnsupportedEncodingException;
21  import java.util.ArrayList;
22  import java.util.Collection;
23  import java.util.HashMap;
24  import java.util.Iterator;
25  import java.util.Map;
26  
27  import org.ops4j.pax.useradmin.provider.ldap.ConfigurationConstants;
28  import org.ops4j.pax.useradmin.service.spi.CredentialProvider;
29  import org.ops4j.pax.useradmin.service.spi.Decryptor;
30  import org.ops4j.pax.useradmin.service.spi.Encryptor;
31  import org.ops4j.pax.useradmin.service.spi.StorageException;
32  import org.ops4j.pax.useradmin.service.spi.StorageProvider;
33  import org.ops4j.pax.useradmin.service.spi.UserAdminFactory;
34  import org.ops4j.pax.useradmin.service.spi.UserAdminTools;
35  import org.osgi.service.cm.ConfigurationException;
36  import org.osgi.service.useradmin.Group;
37  import org.osgi.service.useradmin.Role;
38  import org.osgi.service.useradmin.User;
39  
40  import com.novell.ldap.LDAPAttribute;
41  import com.novell.ldap.LDAPAttributeSet;
42  import com.novell.ldap.LDAPConnection;
43  import com.novell.ldap.LDAPEntry;
44  import com.novell.ldap.LDAPException;
45  import com.novell.ldap.LDAPModification;
46  import com.novell.ldap.LDAPSearchResults;
47  
48  /**
49   * A LDAP based implementation of the <code>StorageProvider</code> SPI.
50   * 
51   * @author Matthias Kuespert
52   * @since 02.07.2009
53   */
54  public class StorageProviderImpl implements StorageProvider, CredentialProvider {
55  
56      private static final String DEFAULT_CREDENTIAL_NAME     = "default";
57      private static final int    CREDENTIAL_VALUE_ARRAY_SIZE = 3;
58  
59      private static final String BASIC_EXT                   = ".basic";
60      private static final String REQUIRED_EXT                = ".required";
61  
62      private static final String PATTERN_SPLIT_LIST_VALUE    = "[;,] *";
63  
64      // configuration
65  
66      private String              m_accessUser                = "";
67      private String              m_accessPassword            = "";
68      private String              m_host                      = ConfigurationConstants.DEFAULT_LDAP_SERVER_URL;
69      private String              m_port                      = ConfigurationConstants.DEFAULT_LDAP_SERVER_PORT;
70  
71      private String              m_rootDN                    = ConfigurationConstants.DEFAULT_LDAP_ROOT_DN;
72      private String              m_rootUsersDN               = ConfigurationConstants.DEFAULT_LDAP_ROOT_USERS + "," + m_rootDN;
73      private String              m_rootGroupsDN              = ConfigurationConstants.DEFAULT_LDAP_ROOT_GROUPS + "," + m_rootDN;
74  
75      private String              m_userObjectclass           = ConfigurationConstants.DEFAULT_USER_OBJECTCLASS;
76      private String              m_userIdAttr                = ConfigurationConstants.DEFAULT_USER_ATTR_ID;
77      private String              m_userMandatoryAttr         = ConfigurationConstants.DEFAULT_USER_ATTR_MANDATORY;
78      private final String        m_userCredentialAttr        = ConfigurationConstants.DEFAULT_USER_ATTR_CREDENTIAL;
79  
80      private String              m_groupObjectclass          = ConfigurationConstants.DEFAULT_GROUP_OBJECTCLASS;
81      private String              m_groupIdAttr               = ConfigurationConstants.DEFAULT_GROUP_ATTR_ID;
82      private String              m_groupMandatoryAttr        = ConfigurationConstants.DEFAULT_GROUP_ATTR_MANDATORY;
83      private String              m_groupCredentialAttr       = ConfigurationConstants.DEFAULT_GROUP_ATTR_CREDENTIAL;
84  
85      private String              m_groupEntryObjectclass     = ConfigurationConstants.DEFAULT_GROUP_ENTRY_OBJECTCLASS;
86      private String              m_groupEntryIdAttr          = ConfigurationConstants.DEFAULT_GROUP_ENTRY_ATTR_ID;
87      private String              m_groupEntryMemberAttr      = ConfigurationConstants.DEFAULT_GROUP_ENTRY_ATTR_MEMBER;
88  
89      /**
90       * The connection which is used for access.
91       */
92      private LDAPConnection      m_connection                = null;
93  
94      /**
95       * Constructor.
96       * 
97       * @param connection
98       *            The LDAP connection to be used by this provider.
99       */
100     protected StorageProviderImpl(LDAPConnection connection) {
101         if (null == connection) {
102             throw new IllegalArgumentException("Internal error: no LDAPConnection object specified when constructing the StorageProvider instance.");
103         }
104         m_connection = connection;
105     }
106 
107     // StorageProvider interface implementation
108     //
109     // - private methods
110 
111     /**
112      * Opens a connection to the LDAP server. Each public method implementation
113      * of this <code>StorageProvider</code> must open and close a connection to
114      * the LDAP server.
115      * 
116      * @see StorageProviderImpl#closeConnection()
117      * @return An initialized connection.
118      * @throws StorageException
119      *             If the connection could not be initialized.
120      */
121     private LDAPConnection openConnection() throws StorageException {
122         try {
123             if (m_connection.isConnected() || m_connection.isBound()) {
124                 m_connection.disconnect();
125             }
126             m_connection.connect(m_host, new Integer(m_port));
127             m_connection.bind(LDAPConnection.LDAP_V3, m_accessUser, m_accessPassword.getBytes("UTF8"));
128             return m_connection;
129         } catch (LDAPException e) {
130             throw new StorageException("Error opening connection to LDAP server '" + m_host + ":" + m_port + "': " + e.getMessage() + " - "
131                     + e.getLDAPErrorMessage());
132         } catch (UnsupportedEncodingException e) {
133             throw new StorageException("Unknown encoding when opening connection: " + e.getMessage());
134         }
135     }
136 
137     /**
138      * Closes the current connection. Each public method implementation of this
139      * <code>StorageProvider</code> must open and close a connection to the LDAP
140      * server.
141      * 
142      * @see StorageProviderImpl#openConnection()
143      * @throws StorageException
144      */
145     private void closeConnection() throws StorageException {
146         try {
147             m_connection.disconnect();
148         } catch (LDAPException e) {
149             throw new StorageException("Error closing connection: " + e.getMessage());
150         }
151     }
152 
153     /**
154      * Returns a DN for the given user name.
155      * 
156      * @param userName
157      *            A valid user name.
158      * @return A DN that identifies a valid user.
159      */
160     private String getUserDN(String userName) {
161         return m_userIdAttr + "=" + userName + "," + m_rootUsersDN;
162     }
163 
164     /**
165      * Returns a DN for the given group name.
166      * 
167      * @param groupName
168      *            A valid group name.
169      * @return A DN that identifies a valid group.
170      */
171     private String getGroupDN(String groupName) {
172         return m_groupIdAttr + "=" + groupName + "," + m_rootGroupsDN;
173     }
174 
175     /**
176      * Returns a DN for a sub-group of the given group name.
177      * 
178      * @param groupName
179      *            A valid group name.
180      * @param ext
181      *            The extension that identifies the sub-group.
182      * @return A DN that identifies a valid sub-group.
183      */
184     private String getGroupDN(String groupName, String ext) {
185         return m_groupEntryIdAttr + "=" + groupName + ext + "," + m_groupIdAttr + "=" + groupName + "," + m_rootGroupsDN;
186     }
187 
188     /**
189      * Returns the DN for the given role.
190      * 
191      * @param role
192      *            The role to lookup.
193      * @return A valid DN the identifies the role.
194      * @throws StorageException
195      *             if the type of the role is not <code>Role.USER</code> or
196      *             <code>Role.GROUP</code>.
197      */
198     private String getRoleDN(Role role) throws StorageException {
199         String dn;
200         switch (role.getType()) {
201             case Role.USER:
202                 dn = getUserDN(role.getName());
203                 break;
204             case Role.GROUP:
205                 dn = getGroupDN(role.getName());
206                 break;
207             default:
208                 throw new StorageException("Invalid role type '" + role.getType() + "'");
209         }
210         return dn;
211     }
212 
213     /**
214      * Returns true if all the configured objectclasses are included in the
215      * given objectclass list.
216      * 
217      * @param configuredClasslist
218      *            The classes to check for.
219      * @param objectClasses
220      *            The list to check.
221      * @return True if all classes are found in the list.
222      */
223     private boolean configuredClassedContainedInObjectClasses(String configuredClasslist, String[] objectClasses) {
224         for (String typeName : configuredClasslist.split(PATTERN_SPLIT_LIST_VALUE)) {
225             boolean contained = false;
226             for (String objectClass : objectClasses) {
227                 if (objectClass.trim().equals(typeName.trim())) {
228                     contained = true;
229                     break;
230                 }
231             }
232             if (!contained) {
233                 return false;
234             }
235         }
236         return true;
237     }
238 
239     /**
240      * Calculates a role type from the list of object classes specified for the
241      * given LDAP entry.
242      * 
243      * @param entry
244      *            The LDAP entry to check.
245      * @return A role type as specified by the Role interface.
246      * @throws StorageException
247      *             if the role type could not be determined.
248      */
249     private int getRoleType(LDAPEntry entry) throws StorageException {
250         LDAPAttribute typeAttr = entry.getAttribute(ConfigurationConstants.ATTR_OBJECTCLASS);
251         if (null == typeAttr) {
252             throw new StorageException("No type attribute '" + ConfigurationConstants.ATTR_OBJECTCLASS + "' found for entry: " + entry);
253         }
254         String[] objectClasses = typeAttr.getStringValueArray();
255         //
256         // check if all our required objectclasses are contained in the
257         // given list
258         if (configuredClassedContainedInObjectClasses(m_userObjectclass, objectClasses)) {
259             return Role.USER;
260         }
261         if (configuredClassedContainedInObjectClasses(m_groupObjectclass, objectClasses)) {
262             return Role.GROUP;
263         }
264         // error handling ...
265         String classes = "";
266         for (String clazz : objectClasses) {
267             if (classes.length() > 0) {
268                 classes += ", ";
269             }
270             classes += clazz;
271         }
272         throw new StorageException("Could not determine role type for objectClasses: '" + classes + "'.");
273     }
274 
275     /**
276      * Creates a Role for the given LDAP entry.
277      * 
278      * @param factory
279      *            The factory to use for object creation.
280      * @param entry
281      *            The entry to create a role for.
282      * @return The created role or null.
283      * @throws StorageException
284      *             if the entry does not map to a role.
285      */
286     @SuppressWarnings(value = "unchecked")
287     private Role createRole(UserAdminFactory factory, LDAPEntry entry) throws StorageException {
288         Map<String, Object> properties = new HashMap<String, Object>();
289         Map<String, Object> credentials = new HashMap<String, Object>();
290         // first determine the type from the objectclasses
291         int type = getRoleType(entry);
292         // then read additional attributes
293         Iterator<LDAPAttribute> it = entry.getAttributeSet().iterator();
294         while (it.hasNext()) {
295             LDAPAttribute attribute = it.next();
296             /* if (ConfigurationConstants.ATTR_OBJECTCLASS.equals(attribute.getName())) {
297                 // ignore: we've read that already
298                 // System.err.println("------------- ignore: " + attribute.getName());
299             } else */
300             if ((type == Role.GROUP && m_groupCredentialAttr.equals(attribute.getName()))
301                     || (type == Role.USER && m_userCredentialAttr.equals(attribute.getName()))) {
302                 for (String value : attribute.getStringValueArray()) {
303                     String[] data = value.split(PATTERN_SPLIT_LIST_VALUE);
304                     if (CREDENTIAL_VALUE_ARRAY_SIZE != data.length) {
305                         throw new StorageException("Wrong credential format '" + value + "' found for entry: " + entry);
306                     }
307                     // ignore default credential for groups
308                     if (type != Role.GROUP || !DEFAULT_CREDENTIAL_NAME.equals(data[1])) {
309                         credentials.put(data[1], ("char".equals(data[0]) ? data[2] : data[2].getBytes()));
310                     }
311                 }
312             } else {
313                 // TODO: how to get the attribute type (String or byte[])?
314                 //
315                 // For now we always read string values ... see Jira issue PAXUSERADMIN-XXX
316                 //
317                 boolean isByteArray = false;
318                 properties.put(attribute.getName(), isByteArray ? attribute.getByteValue() : attribute.getStringValue());
319             }
320         }
321         switch (type) {
322             case Role.USER:
323                 return factory.createUser(entry.getAttribute(m_userIdAttr).getStringValue(), properties, credentials.keySet());
324             case Role.GROUP:
325                 return factory.createGroup(entry.getAttribute(m_groupIdAttr).getStringValue(), properties, credentials.keySet());
326             default:
327                 // should never happen: getRoleType() throws on this
328                 throw new StorageException("Unexpected role type '" + type + "' (0==Role) detected.");
329         }
330     }
331 
332     /**
333      * Returns the entry with the given DN if it exists, null otherwise.
334      * 
335      * @param connection
336      *            The LDAP connection to use.
337      * @param dn
338      * @return The LDAP Entry that represents the given DN - null otherwise.
339      * @throws LDAPException
340      *             if an error occurs when accessing the LDAP server
341      */
342     private LDAPEntry getEntry(LDAPConnection connection, String dn) throws LDAPException {
343         LDAPEntry entry = null;
344         try {
345             entry = connection.read(dn);
346         } catch (LDAPException e) {
347             if (e.getResultCode() != LDAPException.NO_SUCH_OBJECT) {
348                 // re-throw other errors
349                 throw e;
350             } // else ignore and return null
351         }
352         return entry;
353     }
354 
355     /**
356      * Retrieves an LDAP entry based on the given name.
357      * 
358      * @param connection
359      *            The LDAP connection to use.
360      * @param name
361      *            The name of the entry to search for.
362      * @return The LDAP entry that matches the given name.
363      * @throws LDAPException
364      *             if an error occurs when accessing the LDAP server
365      */
366     private LDAPEntry getEntryForName(LDAPConnection connection, String name) throws LDAPException {
367         // first check if a group exists ...
368         LDAPEntry entry = getEntry(connection, getGroupDN(name));
369         if (null == entry) {
370             // check for a user ...
371             entry = getEntry(connection, getUserDN(name));
372         }
373         return entry;
374     }
375 
376     /**
377      * Creates an sub-group entry for a group. Sub-group entries are stored in
378      * two entries below the group node: the 'basic' or 'required' group
379      * entries.
380      * 
381      * @param connection
382      *            The LDAP connection to use.
383      * @param isBasic
384      *            True if the initialMember should be added to the 'basic'
385      *            members - on false it is added to the 'required' members.
386      * @param group
387      *            The group to modify.
388      * @param initialMember
389      *            The initial member to add to this group.
390      * @return The LDAP entry that was created for this group entry.
391      * @throws LDAPException
392      *             if an LDAP error occurs.
393      */
394     private LDAPEntry createGroupEntry(LDAPConnection connection, String entryName, Group group, Role initialMember) throws LDAPException, StorageException {
395         // set objectclass attributes
396         //
397         LDAPAttributeSet attributes = new LDAPAttributeSet();
398         attributes.add(new LDAPAttribute(ConfigurationConstants.ATTR_OBJECTCLASS, m_groupEntryObjectclass.split(PATTERN_SPLIT_LIST_VALUE)));
399         // set ID attribute
400         //
401         attributes.add(new LDAPAttribute(m_groupEntryIdAttr, entryName));
402         //
403         // add initial user
404         //
405         String initialMemberDN = getRoleDN(initialMember);
406         attributes.add(new LDAPAttribute(m_groupEntryMemberAttr, initialMemberDN));
407         //
408         // set all mandatory attributes to name
409         //
410         //        if (!"".equals(m_groupEntryMandatoryAttr)) {
411         //            for (String attr : m_groupEntryMandatoryAttr.split(PATTERN_SPLIT_LIST_VALUE)) {
412         //                attributes.add(new LDAPAttribute(attr.trim(), entryName));
413         //            }
414         //        }
415         // create and add entry
416         LDAPEntry entry = new LDAPEntry(m_groupEntryIdAttr + "=" + entryName + "," + getGroupDN(group.getName()), attributes);
417         connection.add(entry);
418         return entry;
419     }
420 
421     /**
422      * Retrieves the members of the specified sub-group.
423      * 
424      * @param connection
425      *            The LDAP connection to use.
426      * @param factory
427      *            The factory to use for object creation.
428      * @param group
429      * @param subGroupDN
430      * @return
431      * @throws LDAPException
432      * @throws StorageException
433      */
434     @SuppressWarnings(value = "unchecked")
435     private Collection<Role> getMembers(LDAPConnection connection, UserAdminFactory factory, Group group, String ext) throws LDAPException, StorageException {
436         Collection<Role> roles = new ArrayList<Role>();
437         //
438         // get the group main entry
439         //
440         LDAPEntry groupEntry = getEntry(connection, getGroupDN(group.getName()));
441         if (null == groupEntry) {
442             throw new StorageException("Internal error: entry for group '" + group.getName() + "' could not be retrieved.");
443         }
444         //
445         // if there is a <group-name>.<ext> group return its members
446         //
447         LDAPEntry subGroupEntry = getEntry(connection, getGroupDN(group.getName(), ext));
448         if (null != subGroupEntry) {
449             Iterator<LDAPAttribute> it = subGroupEntry.getAttributeSet().iterator();
450             while (it.hasNext()) {
451                 LDAPAttribute attribute = it.next();
452                 if (m_groupEntryMemberAttr.equals(attribute.getName())) {
453                     for (String userDN : attribute.getStringValueArray()) {
454                         LDAPEntry userEntry = getEntry(connection, userDN);
455                         if (null == userEntry) {
456                             throw new StorageException("Internal error: group member '" + userDN + "' could not be retrieved.");
457                         }
458                         Role role = createRole(factory, userEntry);
459                         roles.add(role);
460                     }
461                 }
462             }
463         }
464         return roles;
465     }
466 
467     @SuppressWarnings(value = "unchecked")
468     private boolean addMember(LDAPConnection connection, Group group, String ext, Role member) throws LDAPException, StorageException {
469         // get the group main entry
470         //
471         LDAPEntry groupEntry = getEntry(connection, getGroupDN(group.getName()));
472         if (null == groupEntry) {
473             throw new StorageException("Internal error: entry for group '" + group.getName() + "' could not be retrieved.");
474         }
475         // if there is no <name>.<ext> group create it
476         LDAPEntry subGroupEntry = getEntry(connection, getGroupDN(group.getName(), ext));
477         if (null == subGroupEntry) {
478             subGroupEntry = createGroupEntry(connection, group.getName() + ext, group, member);
479         } else {
480             // add role to group entry
481             String roleDN = getRoleDN(member);
482             Iterator<LDAPAttribute> it = subGroupEntry.getAttributeSet().iterator();
483             while (it.hasNext()) {
484                 LDAPAttribute attribute = it.next();
485                 if (m_groupEntryMemberAttr.equals(attribute.getName())) {
486                     // check the member values
487                     for (String memberDN : attribute.getStringValueArray()) {
488                         if (roleDN.equals(memberDN)) {
489                             // ignore already existing members
490                             return false;
491                         }
492                     }
493                     // add new member
494                     attribute.addValue(roleDN);
495                     LDAPModification modification = new LDAPModification(LDAPModification.REPLACE, new LDAPAttribute(attribute));
496                     connection.modify(subGroupEntry.getDN(), modification);
497                     break;
498                 }
499             }
500         }
501         return true;
502     }
503 
504     /**
505      * Removes the given DN from the group members.
506      * 
507      * @param connection
508      *            The LDAP connection to use.
509      * @param groupEntry
510      *            The group entry to modify.
511      * @param memberDN
512      *            The DN of the member to remove.
513      * @return True if the member was sucessfully removed - false otherwise.
514      * @throws LDAPException
515      *             if an LDAP error occurs.
516      */
517     @SuppressWarnings(value = "unchecked")
518     private boolean removeGroupMember(LDAPConnection connection, LDAPEntry groupEntry, String memberDN) throws LDAPException {
519         if (null != groupEntry) {
520             Iterator<LDAPAttribute> it = groupEntry.getAttributeSet().iterator();
521             while (it.hasNext()) {
522                 LDAPAttribute attribute = it.next();
523                 if (m_groupEntryMemberAttr.equals(attribute.getName())) {
524                     for (String userDN : attribute.getStringValueArray()) {
525                         if (userDN.equals(memberDN)) {
526                             // found: let's remove this member from the group ...
527                             LDAPModification modification = new LDAPModification(LDAPModification.DELETE, new LDAPAttribute(m_groupEntryMemberAttr, memberDN));
528                             connection.modify(groupEntry.getDN(), modification);
529                             return true;
530                         }
531                     }
532                 }
533             }
534         }
535         return false;
536     }
537 
538     private String createCredentialValueString(String key, Object value) throws StorageException {
539         if (!(value instanceof String || value instanceof byte[])) {
540             throw new StorageException("Invalid type for credential value: " + value.getClass().getName());
541         }
542         boolean isString = value instanceof String;
543         String result = (isString ? "char" : "byte") + ";" + key + ";" + (isString ? value : new String((byte[]) value));
544         // System.err.println("------------ create credential: " + result);
545         return result;
546 
547     }
548 
549     // - public <code>StorageProvider</code> interface implementation
550 
551     /**
552      * @see StorageProvider#createUser(UserAdminFactory, String)
553      */
554     @Override
555     public User createUser(UserAdminFactory factory, String name) throws StorageException {
556         LDAPConnection connection = openConnection();
557         // fill attribute set (for LDAP creation) and properties (for UserAdmin creation)
558         LDAPAttributeSet attributes = new LDAPAttributeSet();
559         Map<String, Object> properties = new HashMap<String, Object>();
560         //
561         attributes.add(new LDAPAttribute(ConfigurationConstants.ATTR_OBJECTCLASS, m_userObjectclass.split(PATTERN_SPLIT_LIST_VALUE)));
562         attributes.add(new LDAPAttribute(m_userIdAttr, name));
563         properties.put(m_userIdAttr, name);
564         // set all mandatory attributes to name
565         if (!"".equals(m_userMandatoryAttr)) {
566             for (String attr : m_userMandatoryAttr.split(PATTERN_SPLIT_LIST_VALUE)) {
567                 attributes.add(new LDAPAttribute(attr.trim(), name));
568                 properties.put(attr.trim(), name);
569             }
570         }
571         //
572         LDAPEntry entry = new LDAPEntry(getUserDN(name), attributes);
573         //
574         try {
575             connection.add(entry);
576             return factory.createUser(name, properties, null);
577         } catch (LDAPException e) {
578             throw new StorageException("Error creating user '" + name + "' " + entry + ": " + e.getMessage() + " / " + e.getLDAPErrorMessage());
579         } finally {
580             closeConnection();
581         }
582     }
583 
584     /**
585      * @see StorageProvider#createGroup(UserAdminFactory, String)
586      */
587     @Override
588     public Group createGroup(UserAdminFactory factory, String name) throws StorageException {
589         // create ou as container for basic and required group objects
590         //
591         LDAPAttributeSet attributes = new LDAPAttributeSet();
592         attributes.add(new LDAPAttribute(ConfigurationConstants.ATTR_OBJECTCLASS, m_groupObjectclass.split(PATTERN_SPLIT_LIST_VALUE)));
593         attributes.add(new LDAPAttribute(m_groupIdAttr, name));
594         //
595         // set all mandatory attributes to name
596         //
597         if (!"".equals(m_groupMandatoryAttr)) {
598             for (String attr : m_groupMandatoryAttr.split(PATTERN_SPLIT_LIST_VALUE)) {
599                 if (attr.equals(m_groupCredentialAttr)) {
600                     // note: the default credential is not visible for the calling UserAdmin!
601                     attributes.add(new LDAPAttribute(attr.trim(), createCredentialValueString(DEFAULT_CREDENTIAL_NAME, name)));
602                 } else {
603                     attributes.add(new LDAPAttribute(attr.trim(), name));
604                 }
605             }
606         }
607         //
608         LDAPEntry entry = new LDAPEntry(getGroupDN(name), attributes);
609         //
610         LDAPConnection connection = openConnection();
611         try {
612             connection.add(entry);
613             Map<String, Object> properties = new HashMap<String, Object>();
614             properties.put(m_groupIdAttr, name);
615             return factory.createGroup(name, properties, null);
616         } catch (LDAPException e) {
617             throw new StorageException("Error creating group '" + name + "' " + entry + ": " + e.getMessage() + " / " + e.getLDAPErrorMessage());
618         } finally {
619             closeConnection();
620         }
621     }
622 
623     /**
624      * @see StorageProvider#deleteRole(Role)
625      */
626     @Override
627     public boolean deleteRole(Role role) throws StorageException {
628         String dn = getRoleDN(role);
629         LDAPConnection connection = openConnection();
630         // todo: check for group memberships??
631         try {
632             connection.delete(dn);
633             return true;
634         } catch (LDAPException e) {
635             throw new StorageException("Error deleting role with name '" + role.getName() + "': " + e.getMessage() + " / " + e.getLDAPErrorMessage());
636         } finally {
637             closeConnection();
638         }
639     }
640 
641     /**
642      * @see StorageProvider#getMembers(UserAdminFactory, Group)
643      */
644     @Override
645     public Collection<Role> getMembers(UserAdminFactory factory, Group group) throws StorageException {
646         LDAPConnection connection = openConnection();
647         try {
648             return getMembers(connection, factory, group, BASIC_EXT);
649         } catch (LDAPException e) {
650             throw new StorageException("Error retrieving role with name '" + group.getName() + "': " + e.getMessage() + " / " + e.getLDAPErrorMessage());
651         } finally {
652             closeConnection();
653         }
654     }
655 
656     /**
657      * @see StorageProvider#getRequiredMembers(UserAdminFactory, Group)
658      */
659     @Override
660     public Collection<Role> getRequiredMembers(UserAdminFactory factory, Group group) throws StorageException {
661         LDAPConnection connection = openConnection();
662         try {
663             return getMembers(connection, factory, group, REQUIRED_EXT);
664         } catch (LDAPException e) {
665             throw new StorageException("Error retrieving role with name '" + group.getName() + "': " + e.getMessage() + " / " + e.getLDAPErrorMessage());
666         } finally {
667             closeConnection();
668         }
669     }
670 
671     /**
672      * @see StorageProvider#addMember(Group, Role)
673      */
674     @Override
675     public boolean addMember(Group group, Role role) throws StorageException {
676         LDAPConnection connection = openConnection();
677         try {
678 
679             return addMember(connection, group, BASIC_EXT, role);
680         } catch (LDAPException e) {
681             throw new StorageException("Error adding member role with name '" + role.getName() + "' to group '" + group.getName() + "': " + e.getMessage()
682                     + " / " + e.getLDAPErrorMessage());
683         } finally {
684             closeConnection();
685         }
686     }
687 
688     /**
689      * @see StorageProvider#addRequiredMember(Group, Role)
690      */
691     @Override
692     public boolean addRequiredMember(Group group, Role role) throws StorageException {
693         LDAPConnection connection = openConnection();
694         try {
695             return addMember(connection, group, REQUIRED_EXT, role);
696         } catch (LDAPException e) {
697             throw new StorageException("Error adding required member role with name '" + role.getName() + "' to group '" + group.getName() + "': "
698                     + e.getMessage() + " / " + e.getLDAPErrorMessage());
699         } finally {
700             closeConnection();
701         }
702     }
703 
704     /**
705      * @see StorageProvider#removeMember(Group, Role)
706      */
707     @Override
708     public boolean removeMember(Group group, Role role) throws StorageException {
709         LDAPConnection connection = openConnection();
710         try {
711             // get the group ou-entry
712             //
713             LDAPEntry groupEntry = getEntry(connection, getGroupDN(group.getName()));
714             if (null == groupEntry) {
715                 throw new StorageException("Internal error: entry for group '" + group.getName() + "' could not be retrieved.");
716             }
717             LDAPEntry basicGroupEntry = getEntry(connection, getGroupDN(group.getName(), BASIC_EXT));
718             LDAPEntry requiredGroupEntry = getEntry(connection, getGroupDN(group.getName(), REQUIRED_EXT));
719             String memberDN = getRoleDN(role);
720             //
721             return removeGroupMember(connection, basicGroupEntry, memberDN) || removeGroupMember(connection, requiredGroupEntry, memberDN);
722 
723         } catch (LDAPException e) {
724             throw new StorageException("Error deleting role with name '" + group.getName() + "': " + e.getMessage() + " / " + e.getLDAPErrorMessage());
725         } finally {
726             closeConnection();
727         }
728     }
729 
730     /**
731      * @see StorageProvider#setRoleAttribute(Role, String, Object)
732      */
733     @Override
734     public void setRoleAttribute(Role role, String key, Object value) throws StorageException {
735         if (ConfigurationConstants.ATTR_OBJECTCLASS.equals(key)) {
736             throw new StorageException("Cannot modify attribute '" + ConfigurationConstants.ATTR_OBJECTCLASS + "' - change the configuration instead.");
737         }
738         if (Role.USER == role.getType() && m_userIdAttr.equals(key)) {
739             throw new StorageException("Cannot modify ID attribute '" + m_userIdAttr + "' - recreate the user instead.");
740         }
741         if (Role.GROUP == role.getType() && m_groupEntryIdAttr.equals(key)) {
742             throw new StorageException("Cannot modify ID attribute '" + m_groupEntryIdAttr + "' - recreate the group instead.");
743         }
744         LDAPConnection connection = openConnection();
745         try {
746             String dn = getRoleDN(role);
747             if (value instanceof String) {
748                 connection.modify(dn, new LDAPModification(LDAPModification.REPLACE, new LDAPAttribute(key, (String) value)));
749             } else if (value instanceof byte[]) {
750                 connection.modify(dn, new LDAPModification(LDAPModification.REPLACE, new LDAPAttribute(key, (byte[]) value)));
751             }
752             // note: from an architectural view we shouldn't throw on this, but user will expect feedback on failed storage,
753             //       so provide an exception that the caller may throw or ignore ... no return value since it's an error.
754             else {
755                 throw new StorageException("Invalid value type '" + value.getClass().getName() + "' - only String or byte[] are allowed.");
756             }
757         } catch (LDAPException e) {
758             throw new StorageException("Error setting attribute '" + key + "' = '" + value + "' for role '" + role.getName() + "': " + e.getMessage() + " / "
759                     + e.getLDAPErrorMessage());
760         } finally {
761             closeConnection();
762         }
763     }
764 
765     /**
766      * @see StorageProvider#removeRoleAttribute(Role, String)
767      */
768     @Override
769     public void removeRoleAttribute(Role role, String key) throws StorageException {
770         if (ConfigurationConstants.ATTR_OBJECTCLASS.equals(key)) {
771             throw new StorageException("Cannot remove '" + ConfigurationConstants.ATTR_OBJECTCLASS + "' attribute - change the configuration instead.");
772         }
773         if (Role.USER == role.getType()) {
774             if (m_userIdAttr.equals(key)) {
775                 throw new StorageException("Cannot remove mandatory ID attribute '" + m_userIdAttr + "'.");
776             } else if (m_userMandatoryAttr.contains(key)) {
777                 throw new StorageException("Cannot remove mandatory attribute '" + key + "'.");
778             }
779         }
780         if (Role.GROUP == role.getType()) {
781             if (m_groupIdAttr.equals(key)) {
782                 throw new StorageException("Cannot remove mandatory ID attribute '" + m_groupIdAttr + "'.");
783             } else if (m_groupMandatoryAttr.contains(key)) {
784                 throw new StorageException("Cannot remove mandatory attribute '" + key + "'.");
785             }
786         }
787         LDAPConnection connection = openConnection();
788         try {
789             String dn = getRoleDN(role);
790             LDAPModification modification = new LDAPModification(LDAPModification.DELETE, new LDAPAttribute(key, ""));
791             connection.modify(dn, modification);
792         } catch (LDAPException e) {
793             throw new StorageException("Error deleting attribute '" + key + "'of role '" + role.getName() + "': " + e.getMessage() + " / "
794                     + e.getLDAPErrorMessage());
795         } finally {
796             closeConnection();
797         }
798     }
799 
800     // TODO: how to detect dynamically which non-mandatory arguments to delete?
801     @Override
802     public void clearRoleAttributes(Role role) throws StorageException {
803         throw new IllegalStateException("clearing attributes is not yet implemented");
804     }
805 
806     @Override
807     public void setUserCredential(Encryptor encryptor, User user, String key, Object value) throws StorageException {
808         LDAPConnection connection = openConnection();
809         try {
810             String dn = getRoleDN(user);
811             LDAPEntry entry = getEntry(connection, dn);
812             if (null == entry) {
813                 throw new StorageException("Could not find user '" + user.getName() + "'");
814             }
815             String attrName = (Role.USER == user.getType()) ? m_userCredentialAttr : m_groupCredentialAttr;
816             LDAPAttribute attribute = entry.getAttribute(attrName);
817             if (null != attribute) {
818                 for (String attrValue : attribute.getStringValueArray()) {
819                     String[] data = attrValue.split(PATTERN_SPLIT_LIST_VALUE);
820                     if (CREDENTIAL_VALUE_ARRAY_SIZE != data.length) {
821                         throw new StorageException("Wrong credential format: could not split into " + CREDENTIAL_VALUE_ARRAY_SIZE + " chunks: '" + value
822                                 + "' - entry: " + entry);
823                     }
824                     if (data[1].equals(key)) {
825                         // modify existing entry
826                         attribute.removeValue(attrValue);
827                     }
828                 }
829                 // TODO: if we get here the value does not yet exist or was removed above - now add it
830                 attribute.addValue(createCredentialValueString(key, value));
831                 LDAPModification modification = new LDAPModification(LDAPModification.REPLACE, attribute);
832                 connection.modify(dn, modification);
833             } else {
834                 LDAPModification modification = new LDAPModification(LDAPModification.ADD, new LDAPAttribute(attrName, createCredentialValueString(key, value)));
835                 connection.modify(dn, modification);
836             }
837         } catch (LDAPException e) {
838             throw new StorageException("Error setting credential for user '" + user.getName() + "': " + e.getMessage() + " / " + e.getLDAPErrorMessage());
839         } finally {
840             closeConnection();
841         }
842     }
843 
844     @Override
845     public void removeUserCredential(User user, String key) throws StorageException {
846         LDAPConnection connection = openConnection();
847         try {
848             String dn = getRoleDN(user);
849             LDAPEntry entry = getEntry(connection, dn);
850             if (null == entry) {
851                 throw new StorageException("Could not find user '" + user.getName() + "'");
852             }
853             String attrName = (Role.USER == user.getType()) ? m_userCredentialAttr : m_groupCredentialAttr;
854             LDAPAttribute attribute = entry.getAttribute(attrName);
855             if (null != attribute) {
856                 for (String attrValue : attribute.getStringValueArray()) {
857                     String[] data = attrValue.split("; *");
858                     if (CREDENTIAL_VALUE_ARRAY_SIZE != data.length) {
859                         throw new StorageException("Wrong credential format '" + attrValue + "' found for entry: " + entry);
860                     }
861                     if (data[1].equals(key)) {
862                         // modify existing entry
863                         // Note: depending on the configured scheme a LDAPException is thrown if the last value is removed
864                         attribute.removeValue(attrValue);
865                         LDAPModification modification = new LDAPModification(LDAPModification.REPLACE, attribute);
866                         connection.modify(dn, modification);
867                         return;
868                     }
869                 }
870             }
871         } catch (LDAPException e) {
872             throw new StorageException("Error setting credential for user '" + user.getName() + "': " + e.getMessage() + " / " + e.getLDAPErrorMessage());
873         } finally {
874             closeConnection();
875         }
876     }
877 
878     @Override
879     public void clearUserCredentials(User user) throws StorageException {
880         throw new IllegalStateException("credential handling is not yet implemented");
881     }
882 
883     /**
884      * @see StorageProvider#getRole(UserAdminFactory, String)
885      */
886     @Override
887     public Role getRole(UserAdminFactory factory, String name) throws StorageException {
888         LDAPConnection connection = openConnection();
889         try {
890             LDAPEntry entry = getEntryForName(connection, name);
891             return null != entry ? createRole(factory, entry) : null;
892         } catch (LDAPException e) {
893             throw new StorageException("Error finding role with name '" + name + "': " + e.getMessage() + " / " + e.getLDAPErrorMessage());
894         } finally {
895             closeConnection();
896         }
897     }
898 
899     /**
900      * @see StorageProvider#getUser(UserAdminFactory, String, String)
901      */
902     @Override
903     public User getUser(UserAdminFactory factory, String key, String value) throws StorageException {
904         LDAPConnection connection = openConnection();
905         try {
906             String filterString = "(&";
907             for (String objectClass : m_userObjectclass.split(PATTERN_SPLIT_LIST_VALUE)) {
908                 filterString += "(" + ConfigurationConstants.ATTR_OBJECTCLASS + "=" + objectClass.trim() + ")";
909             }
910             filterString += "(" + key + "=" + value + "))";
911             LDAPSearchResults result = connection.search(m_rootDN, LDAPConnection.SCOPE_SUB, filterString, null, false);
912             User user = null;
913             while (result.hasMore()) {
914                 if (null != user) {
915                     throw new StorageException("more than one user found");
916                 }
917                 LDAPEntry entry = result.next();
918                 Role role = createRole(factory, entry);
919                 if (null != role) {
920                     if (Role.USER != role.getType()) {
921                         throw new StorageException("Internal error: found role is not a user");
922                     }
923                     user = (User) role;
924                 }
925             }
926             return user;
927         } catch (LDAPException e) {
928             throw new StorageException("Error finding user with attribute '" + key + "=" + value + "': " + e.getMessage() + " / " + e.getLDAPErrorMessage());
929         } finally {
930             closeConnection();
931         }
932     }
933 
934     /**
935      * @see StorageProvider#findRoles(UserAdminFactory, String)
936      */
937     @Override
938     public Collection<Role> findRoles(UserAdminFactory factory, String filterString) throws StorageException {
939         LDAPConnection connection = openConnection();
940         Collection<Role> roles = new ArrayList<Role>();
941         try {
942             LDAPSearchResults result = connection.search(m_rootUsersDN, LDAPConnection.SCOPE_ONE, filterString, null, false);
943             while (result.hasMore()) {
944                 LDAPEntry entry = result.next();
945                 Role role = createRole(factory, entry);
946                 if (null != role) {
947                     roles.add(role);
948                 }
949             }
950             result = connection.search(m_rootGroupsDN, LDAPConnection.SCOPE_ONE, filterString, null, false);
951             while (result.hasMore()) {
952                 LDAPEntry entry = result.next();
953                 Role role = createRole(factory, entry);
954                 if (null != role) {
955                     roles.add(role);
956                 }
957             }
958             return roles;
959         } catch (LDAPException e) {
960             throw new StorageException("Error finding roles with filter '" + filterString + "': " + e.getMessage() + " / " + e.getLDAPErrorMessage());
961         } finally {
962             closeConnection();
963         }
964     }
965 
966     /* (non-Javadoc)
967      * @see org.ops4j.pax.useradmin.service.spi.CredentialProvider#getUserCredential(org.osgi.service.useradmin.User, java.lang.String, java.lang.Object)
968      */
969     @Override
970     public Object getUserCredential(Decryptor decryptor, User user, String key) throws StorageException {
971         int type = user.getType();
972         LDAPConnection connection = openConnection();
973         try {
974             String dn = getRoleDN(user);
975             LDAPEntry entry = getEntry(connection, dn);
976             if (null == entry) {
977                 throw new StorageException("Could not find user '" + user.getName() + "'");
978             }
979             String attrName = (Role.USER == user.getType()) ? m_userCredentialAttr : m_groupCredentialAttr;
980             LDAPAttribute attribute = entry.getAttribute(attrName);
981             if (null != attribute) {
982                 if ((type == Role.GROUP && m_groupCredentialAttr.equals(attribute.getName()))
983                         || (type == Role.USER && m_userCredentialAttr.equals(attribute.getName()))) {
984                     for (String value : attribute.getStringValueArray()) {
985                         String[] data = value.split(PATTERN_SPLIT_LIST_VALUE);
986                         if (CREDENTIAL_VALUE_ARRAY_SIZE != data.length) {
987                             throw new StorageException("Wrong credential format '" + value + "' found for entry: " + entry);
988                         }
989                         // ignore default credential for groups
990                         if (type != Role.GROUP || !DEFAULT_CREDENTIAL_NAME.equals(data[1])) {
991                             return ("char".equals(data[0]) ? data[2] : data[2].getBytes());
992                         }
993                     }
994                 }
995             }
996         } catch (LDAPException e) {
997             throw new StorageException("Error getting credential for user '" + user.getName() + "': " + e.getMessage() + " / " + e.getLDAPErrorMessage());
998         } finally {
999             closeConnection();
1000         }
1001         return null;
1002     }
1003 
1004     /* (non-Javadoc)
1005      * @see org.ops4j.pax.useradmin.service.spi.CredentialProvider#hasUserCredential(org.osgi.service.useradmin.User, java.lang.String, java.lang.Object)
1006      */
1007     @Override
1008     public boolean hasUserCredential(Decryptor decryptor, User user, String key, Object value) throws StorageException {
1009         return value.equals(getUserCredential(null, user, key));
1010     }
1011 
1012     /* (non-Javadoc)
1013      * @see org.ops4j.pax.useradmin.service.spi.StorageProvider#getCredentialProvider()
1014      */
1015     @Override
1016     public CredentialProvider getCredentialProvider() {
1017         return this;
1018     }
1019 
1020     /* (non-Javadoc)
1021      * @see org.ops4j.pax.useradmin.service.spi.StorageProvider#configurationUpdated(java.util.Map)
1022      */
1023     @Override
1024     public void configurationUpdated(Map<String, ?> properties) throws ConfigurationException {
1025         if (null == properties) {
1026             // ignore empty properties
1027             return;
1028         }
1029         //
1030         m_accessUser = UserAdminTools.getOptionalProperty(properties, ConfigurationConstants.PROP_LDAP_ACCESS_USER, "");
1031         m_accessPassword = UserAdminTools.getOptionalProperty(properties, ConfigurationConstants.PROP_LDAP_ACCESS_PWD, "");
1032         //
1033         m_host = UserAdminTools.getOptionalProperty(properties, ConfigurationConstants.PROP_LDAP_SERVER_URL, ConfigurationConstants.DEFAULT_LDAP_SERVER_URL);
1034         m_port = UserAdminTools.getOptionalProperty(properties, ConfigurationConstants.PROP_LDAP_SERVER_PORT, ConfigurationConstants.DEFAULT_LDAP_SERVER_PORT);
1035         //
1036         m_rootDN = UserAdminTools.getMandatoryProperty(properties, ConfigurationConstants.PROP_LDAP_ROOT_DN);
1037         m_rootUsersDN = UserAdminTools.getOptionalProperty(properties, ConfigurationConstants.PROP_LDAP_ROOT_USERS, ConfigurationConstants.DEFAULT_LDAP_ROOT_USERS)
1038                 + "," + m_rootDN;
1039         m_rootGroupsDN = UserAdminTools.getOptionalProperty(properties, ConfigurationConstants.PROP_LDAP_ROOT_GROUPS, ConfigurationConstants.DEFAULT_LDAP_ROOT_GROUPS)
1040                 + "," + m_rootDN;
1041 
1042         m_userObjectclass = UserAdminTools.getOptionalProperty(properties, ConfigurationConstants.PROP_USER_OBJECTCLASS, ConfigurationConstants.DEFAULT_USER_OBJECTCLASS);
1043         m_userIdAttr = UserAdminTools.getOptionalProperty(properties, ConfigurationConstants.PROP_USER_ATTR_ID, ConfigurationConstants.DEFAULT_USER_ATTR_ID);
1044         m_userMandatoryAttr = UserAdminTools.getOptionalProperty(properties, ConfigurationConstants.PROP_USER_ATTR_MANDATORY, ConfigurationConstants.DEFAULT_USER_ATTR_MANDATORY);
1045 
1046         m_groupObjectclass = UserAdminTools.getOptionalProperty(properties, ConfigurationConstants.PROP_GROUP_OBJECTCLASS, ConfigurationConstants.DEFAULT_GROUP_OBJECTCLASS);
1047         m_groupIdAttr = UserAdminTools.getOptionalProperty(properties, ConfigurationConstants.PROP_GROUP_ATTR_ID, ConfigurationConstants.DEFAULT_GROUP_ATTR_ID);
1048         m_groupMandatoryAttr = UserAdminTools.getOptionalProperty(properties, ConfigurationConstants.PROP_GROUP_ATTR_MANDATORY, ConfigurationConstants.DEFAULT_GROUP_ATTR_MANDATORY);
1049         m_groupCredentialAttr = UserAdminTools.getOptionalProperty(properties, ConfigurationConstants.PROP_GROUP_ATTR_CREDENTIAL, ConfigurationConstants.DEFAULT_GROUP_ATTR_CREDENTIAL);
1050 
1051         m_groupEntryObjectclass = UserAdminTools.getOptionalProperty(properties, ConfigurationConstants.PROP_GROUP_ENTRY_OBJECTCLASS, ConfigurationConstants.DEFAULT_GROUP_ENTRY_OBJECTCLASS);
1052         m_groupEntryIdAttr = UserAdminTools.getOptionalProperty(properties, ConfigurationConstants.PROP_GROUP_ENTRY_ATTR_ID, ConfigurationConstants.DEFAULT_GROUP_ENTRY_ATTR_ID);
1053         m_groupEntryMemberAttr = UserAdminTools.getOptionalProperty(properties, ConfigurationConstants.PROP_GROUP_ENTRY_ATTR_MEMBER, ConfigurationConstants.DEFAULT_GROUP_ENTRY_ATTR_MEMBER);
1054 
1055     }
1056 }