--- /dev/null
+package org.argeo.cms.directory.ldap;
+
+import static org.argeo.api.acr.ldap.LdapAttrs.objectClass;
+import static org.argeo.api.acr.ldap.LdapObjs.inetOrgPerson;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.NavigableMap;
+import java.util.Objects;
+import java.util.Set;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+import javax.naming.ldap.LdapName;
+
+import org.argeo.api.acr.ldap.LdapObjs;
+import org.argeo.api.cms.directory.HierarchyUnit;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkUtil;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.service.useradmin.Role;
+
+/** A user admin based on a LDIF files. */
+public class LdifDao extends AbstractLdapDirectoryDao {
+ private NavigableMap<LdapName, LdapEntry> entries = new TreeMap<>();
+ private NavigableMap<LdapName, LdapHierarchyUnit> hierarchy = new TreeMap<>();
+
+ private NavigableMap<LdapName, Attributes> values = new TreeMap<>();
+
+ public LdifDao(AbstractLdapDirectory directory) {
+ super(directory);
+ }
+
+ public void init() {
+ String uri = getDirectory().getUri();
+ if (uri == null)
+ return;
+ try {
+ URI u = new URI(uri);
+ if (u.getScheme().equals("file")) {
+ File file = new File(u);
+ if (!file.exists())
+ return;
+ }
+ load(u.toURL().openStream());
+ } catch (IOException | URISyntaxException e) {
+ throw new IllegalStateException("Cannot open URL " + getDirectory().getUri(), e);
+ }
+ }
+
+ public void save() {
+ if (getDirectory().getUri() == null)
+ return; // ignore
+ if (getDirectory().isReadOnly())
+ throw new IllegalStateException(
+ "Cannot save LDIF user admin: " + getDirectory().getUri() + " is read-only");
+ try (FileOutputStream out = new FileOutputStream(new File(new URI(getDirectory().getUri())))) {
+ save(out);
+ } catch (IOException | URISyntaxException e) {
+ throw new IllegalStateException("Cannot save user admin to " + getDirectory().getUri(), e);
+ }
+ }
+
+ public void save(OutputStream out) throws IOException {
+ try {
+ LdifWriter ldifWriter = new LdifWriter(out);
+ for (LdapName name : hierarchy.keySet())
+ ldifWriter.writeEntry(name, hierarchy.get(name).getAttributes());
+ for (LdapName name : entries.keySet())
+ ldifWriter.writeEntry(name, entries.get(name).getAttributes());
+ } finally {
+ out.close();
+ }
+ }
+
+ public void load(InputStream in) {
+ try {
+ entries.clear();
+ hierarchy.clear();
+
+ LdifParser ldifParser = new LdifParser();
+ SortedMap<LdapName, Attributes> allEntries = ldifParser.read(in);
+ for (LdapName key : allEntries.keySet()) {
+ Attributes attributes = allEntries.get(key);
+ // check for inconsistency
+ Set<String> lowerCase = new HashSet<String>();
+ NamingEnumeration<String> ids = attributes.getIDs();
+ while (ids.hasMoreElements()) {
+ String id = ids.nextElement().toLowerCase();
+ if (lowerCase.contains(id))
+ throw new IllegalStateException(key + " has duplicate id " + id);
+ lowerCase.add(id);
+ }
+
+ values.put(key, attributes);
+
+ // analyse object classes
+ NamingEnumeration<?> objectClasses = attributes.get(objectClass.name()).getAll();
+ // System.out.println(key);
+ objectClasses: while (objectClasses.hasMore()) {
+ String objectClass = objectClasses.next().toString();
+ // System.out.println(" " + objectClass);
+ if (objectClass.toLowerCase().equals(inetOrgPerson.name().toLowerCase())) {
+ entries.put(key, newUser(key));
+ break objectClasses;
+ } else if (objectClass.toLowerCase().equals(getDirectory().getGroupObjectClass().toLowerCase())) {
+ entries.put(key, newGroup(key));
+ break objectClasses;
+ } else if (objectClass.equalsIgnoreCase(LdapObjs.organizationalUnit.name())) {
+ // TODO skip if it does not contain groups or users
+ hierarchy.put(key, new LdapHierarchyUnit(getDirectory(), key));
+ break objectClasses;
+ }
+ }
+ }
+
+ } catch (NamingException | IOException e) {
+ throw new IllegalStateException("Cannot load user admin service from LDIF", e);
+ }
+ }
+
+ public void destroy() {
+// if (users == null || groups == null)
+ if (entries == null)
+ throw new IllegalStateException("User directory " + getDirectory().getBaseDn() + " is already destroyed");
+// users = null;
+// groups = null;
+ entries = null;
+ }
+
+ /*
+ * USER ADMIN
+ */
+
+ @Override
+ public LdapEntry doGetEntry(LdapName key) throws NameNotFoundException {
+ if (entries.containsKey(key))
+ return entries.get(key);
+ throw new NameNotFoundException(key + " not persisted");
+ }
+
+ @Override
+ public Attributes doGetAttributes(LdapName name) {
+ if (!values.containsKey(name))
+ throw new IllegalStateException(name + " doe not exist in " + getDirectory().getBaseDn());
+ return values.get(name);
+ }
+
+ @Override
+ public boolean checkConnection() {
+ return true;
+ }
+
+ @Override
+ public boolean entryExists(LdapName dn) {
+ return entries.containsKey(dn);// || groups.containsKey(dn);
+ }
+
+ @Override
+ public List<LdapEntry> doGetEntries(LdapName searchBase, String f, boolean deep) {
+ Objects.requireNonNull(searchBase);
+ ArrayList<LdapEntry> res = new ArrayList<>();
+ if (f == null && deep && getDirectory().getBaseDn().equals(searchBase)) {
+ res.addAll(entries.values());
+ } else {
+ filterRoles(entries, searchBase, f, deep, res);
+ }
+ return res;
+ }
+
+ private void filterRoles(SortedMap<LdapName, ? extends LdapEntry> map, LdapName searchBase, String f, boolean deep,
+ List<LdapEntry> res) {
+ // FIXME get rid of OSGi references
+ try {
+ // TODO reduce map with search base ?
+ Filter filter = f != null ? FrameworkUtil.createFilter(f) : null;
+ roles: for (LdapEntry user : map.values()) {
+ LdapName dn = user.getDn();
+ if (dn.startsWith(searchBase)) {
+ if (!deep && dn.size() != (searchBase.size() + 1))
+ continue roles;
+ if (filter == null)
+ res.add(user);
+ else {
+ if (user instanceof Role) {
+ if (filter.match(((Role) user).getProperties()))
+ res.add(user);
+ }
+ }
+ }
+ }
+ } catch (InvalidSyntaxException e) {
+ throw new IllegalArgumentException("Cannot create filter " + f, e);
+ }
+
+ }
+
+ @Override
+ public List<LdapName> getDirectGroups(LdapName dn) {
+ List<LdapName> directGroups = new ArrayList<LdapName>();
+ entries: for (LdapName name : entries.keySet()) {
+ LdapEntry group;
+ try {
+ LdapEntry entry = doGetEntry(name);
+ if (AbstractLdapDirectory.hasObjectClass(entry.getAttributes(), getDirectory().getGroupObjectClass())) {
+ group = entry;
+ } else {
+ continue entries;
+ }
+ } catch (NameNotFoundException e) {
+ throw new IllegalArgumentException("Group " + dn + " not found", e);
+ }
+ if (group.getReferences(getDirectory().getMemberAttributeId()).contains(dn)) {
+ directGroups.add(group.getDn());
+ }
+ }
+ return directGroups;
+ }
+
+ @Override
+ public void prepare(LdapEntryWorkingCopy wc) {
+ // delete
+ for (LdapName dn : wc.getDeletedData().keySet()) {
+ if (entries.containsKey(dn))
+ entries.remove(dn);
+ else
+ throw new IllegalStateException("User to delete not found " + dn);
+ }
+ // add
+ for (LdapName dn : wc.getNewData().keySet()) {
+ LdapEntry user = (LdapEntry) wc.getNewData().get(dn);
+ if (entries.containsKey(dn))
+ throw new IllegalStateException("User to create found " + dn);
+ entries.put(dn, user);
+ }
+ // modify
+ for (LdapName dn : wc.getModifiedData().keySet()) {
+ Attributes modifiedAttrs = wc.getModifiedData().get(dn);
+ LdapEntry user;
+ try {
+ user = doGetEntry(dn);
+ } catch (NameNotFoundException e) {
+ throw new IllegalStateException("User to modify no found " + dn, e);
+ }
+ if (user == null)
+ throw new IllegalStateException("User to modify no found " + dn);
+ user.publishAttributes(modifiedAttrs);
+ }
+ }
+
+ @Override
+ public void commit(LdapEntryWorkingCopy wc) {
+ save();
+ }
+
+ @Override
+ public void rollback(LdapEntryWorkingCopy wc) {
+ init();
+ }
+
+ /*
+ * HIERARCHY
+ */
+ @Override
+ public HierarchyUnit doGetHierarchyUnit(LdapName dn) {
+ if (getDirectory().getBaseDn().equals(dn))
+ return getDirectory();
+ return hierarchy.get(dn);
+ }
+
+ @Override
+ public Iterable<HierarchyUnit> doGetDirectHierarchyUnits(LdapName searchBase, boolean functionalOnly) {
+ List<HierarchyUnit> res = new ArrayList<>();
+ for (LdapName n : hierarchy.keySet()) {
+ if (n.size() == searchBase.size() + 1) {
+ if (n.startsWith(searchBase)) {
+ HierarchyUnit hu = hierarchy.get(n);
+ if (functionalOnly) {
+ if (hu.isFunctional())
+ res.add(hu);
+ } else {
+ res.add(hu);
+ }
+ }
+ }
+ }
+ return res;
+ }
+
+ public void scope(LdifDao scoped) {
+ scoped.entries = Collections.unmodifiableNavigableMap(entries);
+ }
+}