From e168383bac50637131fef8c41e119db7eb2284a7 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Wed, 15 Jun 2022 11:15:52 +0200 Subject: [PATCH] Introduce hierarchies in user management --- .../cms/swt/widgets/AbstractSwtPart.java | 14 +++ .../cms/swt/widgets/SwtHierarchicalPart.java | 33 +++++-- .../argeo/cms/swt/widgets/SwtTabularPart.java | 89 ++++++++++++++++++ .../org/argeo/cms/ux/widgets/ColumnsPart.java | 5 + .../org/argeo/cms/ux/widgets/DataPart.java | 15 +++ .../cms/ux/widgets/HierarchicalPart.java | 2 +- .../org/argeo/cms/ux/widgets/TabularPart.java | 5 + .../src/org/argeo/cms/CmsUserManager.java | 6 +- .../src/org/argeo/cms/auth/CurrentUser.java | 12 ++- .../cms/internal/auth/CmsUserManagerImpl.java | 7 ++ .../argeo/cms/internal/runtime/InitUtils.java | 3 +- .../osgi/useradmin/AbstractUserDirectory.java | 89 +++++++++++++++++- .../argeo/osgi/useradmin/HierarchyUnit.java | 24 +++++ .../argeo/osgi/useradmin/LdapUserAdmin.java | 7 +- .../osgi/useradmin/LdifHierarchyUnit.java | 91 +++++++++++++++++++ .../argeo/osgi/useradmin/LdifUserAdmin.java | 85 +++++++++++++++-- .../argeo/osgi/useradmin/OsUserDirectory.java | 2 +- .../argeo/osgi/useradmin/UserDirectory.java | 4 +- .../org/argeo/cms/web/CmsWebEntryPoint.java | 2 + 19 files changed, 460 insertions(+), 35 deletions(-) create mode 100644 eclipse/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/AbstractSwtPart.java create mode 100644 eclipse/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/SwtTabularPart.java create mode 100644 org.argeo.cms.ux/src/org/argeo/cms/ux/widgets/ColumnsPart.java create mode 100644 org.argeo.cms.ux/src/org/argeo/cms/ux/widgets/DataPart.java create mode 100644 org.argeo.cms.ux/src/org/argeo/cms/ux/widgets/TabularPart.java create mode 100644 org.argeo.util/src/org/argeo/osgi/useradmin/HierarchyUnit.java create mode 100644 org.argeo.util/src/org/argeo/osgi/useradmin/LdifHierarchyUnit.java diff --git a/eclipse/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/AbstractSwtPart.java b/eclipse/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/AbstractSwtPart.java new file mode 100644 index 000000000..e846bb54e --- /dev/null +++ b/eclipse/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/AbstractSwtPart.java @@ -0,0 +1,14 @@ +package org.argeo.cms.swt.widgets; + +import org.argeo.cms.swt.CmsSwtUtils; +import org.eclipse.swt.widgets.Composite; + +public abstract class AbstractSwtPart { + protected final Composite area; + + public AbstractSwtPart(Composite parent, int style) { + area = new Composite(parent, style); + area.setLayout(CmsSwtUtils.noSpaceGridLayout()); + } + +} diff --git a/eclipse/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/SwtHierarchicalPart.java b/eclipse/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/SwtHierarchicalPart.java index d390b5364..08c85b165 100644 --- a/eclipse/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/SwtHierarchicalPart.java +++ b/eclipse/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/SwtHierarchicalPart.java @@ -12,28 +12,30 @@ import org.eclipse.swt.widgets.Tree; import org.eclipse.swt.widgets.TreeItem; /** {@link HierarchicalPart} implementation based on a {@link Tree}. */ -public class SwtHierarchicalPart extends Composite implements HierarchicalPart { - private static final long serialVersionUID = -2189742596757101778L; - +public class SwtHierarchicalPart implements HierarchicalPart { + private Composite area; private final Tree tree; private Consumer onSelected; private Consumer onAction; public SwtHierarchicalPart(Composite parent, int style) { - super(parent, style); - setLayout(CmsSwtUtils.noSpaceGridLayout()); - tree = new Tree(this, SWT.VIRTUAL | SWT.BORDER); + area = new Composite(parent, style); + area.setLayout(CmsSwtUtils.noSpaceGridLayout()); + tree = new Tree(area, SWT.VIRTUAL | SWT.BORDER); } + @Override public void refresh() { + // TODO optimise + tree.clearAll(true); tree.addListener(SWT.SetData, event -> { TreeItem item = (TreeItem) event.item; TreeItem parentItem = item.getParentItem(); if (parentItem == null) { refreshRootItem(item); } else { - refreshItem(parentItem, parentItem); + refreshItem(parentItem, item); } }); tree.setItemCount(getRootItemCount()); @@ -45,17 +47,30 @@ public class SwtHierarchicalPart extends Composite implements HierarchicalPart { @Override public void widgetSelected(SelectionEvent e) { - onSelected.accept(e.item.getData()); + if (onSelected != null) + onSelected.accept(e.item.getData()); } @Override public void widgetDefaultSelected(SelectionEvent e) { - onAction.accept(e.item.getData()); + if (onAction != null) + onAction.accept(e.item.getData()); } }); } + @Override + public void setInput(Object data) { + area.setData(data); + refresh(); + } + + @Override + public Object getInput() { + return area.getData(); + } + protected void refreshRootItem(TreeItem item) { } diff --git a/eclipse/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/SwtTabularPart.java b/eclipse/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/SwtTabularPart.java new file mode 100644 index 000000000..02f2f8153 --- /dev/null +++ b/eclipse/org.argeo.cms.swt/src/org/argeo/cms/swt/widgets/SwtTabularPart.java @@ -0,0 +1,89 @@ +package org.argeo.cms.swt.widgets; + +import java.util.function.Consumer; + +import org.argeo.cms.swt.CmsSwtUtils; +import org.argeo.cms.ux.widgets.TabularPart; +import org.eclipse.swt.SWT; +import org.eclipse.swt.events.SelectionEvent; +import org.eclipse.swt.events.SelectionListener; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableItem; + +/** {@link TabularPart} implementation based on a {@link Table}. */ +public class SwtTabularPart implements TabularPart { + private Composite area; + + private final Table table; + + private Consumer onSelected; + private Consumer onAction; + + public SwtTabularPart(Composite parent, int style) { + area = new Composite(parent, style); + area.setLayout(CmsSwtUtils.noSpaceGridLayout()); + table = new Table(area, SWT.VIRTUAL | SWT.BORDER); + } + + @Override + public void refresh() { + // TODO optimise + table.clearAll(); + table.addListener(SWT.SetData, event -> { + TableItem item = (TableItem) event.item; + refreshItem(item); + }); + table.setItemCount(getItemCount()); + CmsSwtUtils.fill(table); + + table.addSelectionListener(new SelectionListener() { + private static final long serialVersionUID = -5225905921522775948L; + + @Override + public void widgetSelected(SelectionEvent e) { + if (onSelected != null) + onSelected.accept(e.item.getData()); + } + + @Override + public void widgetDefaultSelected(SelectionEvent e) { + if (onAction != null) + onAction.accept(e.item.getData()); + } + }); + + } + + @Override + public void setInput(Object data) { + area.setData(data); + refresh(); + } + + @Override + public Object getInput() { + return area.getData(); + } + + protected void refreshItem(TableItem item) { + + } + + protected int getItemCount() { + return 0; + } + + protected Table getTable() { + return table; + } + + public void onSelected(Consumer onSelected) { + this.onSelected = onSelected; + } + + public void onAction(Consumer onAction) { + this.onAction = onAction; + } + +} diff --git a/org.argeo.cms.ux/src/org/argeo/cms/ux/widgets/ColumnsPart.java b/org.argeo.cms.ux/src/org/argeo/cms/ux/widgets/ColumnsPart.java new file mode 100644 index 000000000..7df7086b3 --- /dev/null +++ b/org.argeo.cms.ux/src/org/argeo/cms/ux/widgets/ColumnsPart.java @@ -0,0 +1,5 @@ +package org.argeo.cms.ux.widgets; + +public interface ColumnsPart extends DataPart { + +} diff --git a/org.argeo.cms.ux/src/org/argeo/cms/ux/widgets/DataPart.java b/org.argeo.cms.ux/src/org/argeo/cms/ux/widgets/DataPart.java new file mode 100644 index 000000000..5a8da469b --- /dev/null +++ b/org.argeo.cms.ux/src/org/argeo/cms/ux/widgets/DataPart.java @@ -0,0 +1,15 @@ +package org.argeo.cms.ux.widgets; + +import java.util.function.Consumer; + +public interface DataPart { + void setInput(Object data); + + Object getInput(); + + void refresh(); + + void onSelected(Consumer onSelected); + + void onAction(Consumer onAction); +} diff --git a/org.argeo.cms.ux/src/org/argeo/cms/ux/widgets/HierarchicalPart.java b/org.argeo.cms.ux/src/org/argeo/cms/ux/widgets/HierarchicalPart.java index 244765eaf..0926ff527 100644 --- a/org.argeo.cms.ux/src/org/argeo/cms/ux/widgets/HierarchicalPart.java +++ b/org.argeo.cms.ux/src/org/argeo/cms/ux/widgets/HierarchicalPart.java @@ -1,5 +1,5 @@ package org.argeo.cms.ux.widgets; -public interface HierarchicalPart { +public interface HierarchicalPart extends ColumnsPart { } diff --git a/org.argeo.cms.ux/src/org/argeo/cms/ux/widgets/TabularPart.java b/org.argeo.cms.ux/src/org/argeo/cms/ux/widgets/TabularPart.java new file mode 100644 index 000000000..40a56eb52 --- /dev/null +++ b/org.argeo.cms.ux/src/org/argeo/cms/ux/widgets/TabularPart.java @@ -0,0 +1,5 @@ +package org.argeo.cms.ux.widgets; + +public interface TabularPart extends ColumnsPart { + +} diff --git a/org.argeo.cms/src/org/argeo/cms/CmsUserManager.java b/org.argeo.cms/src/org/argeo/cms/CmsUserManager.java index cd76d65ef..c51d21caa 100644 --- a/org.argeo.cms/src/org/argeo/cms/CmsUserManager.java +++ b/org.argeo.cms/src/org/argeo/cms/CmsUserManager.java @@ -2,10 +2,12 @@ package org.argeo.cms; import java.time.ZonedDateTime; import java.util.List; +import java.util.Map; import java.util.Set; import javax.security.auth.Subject; +import org.argeo.osgi.useradmin.UserDirectory; import org.osgi.framework.InvalidSyntaxException; import org.osgi.service.useradmin.Role; import org.osgi.service.useradmin.User; @@ -15,7 +17,9 @@ import org.osgi.service.useradmin.User; * the userAdmin. */ public interface CmsUserManager { - + public Map getKnownBaseDns(boolean onlyWritable); + public Set getUserDirectories(); + // CurrentUser /** Returns the e-mail of the current logged in user */ public String getMyMail(); diff --git a/org.argeo.cms/src/org/argeo/cms/auth/CurrentUser.java b/org.argeo.cms/src/org/argeo/cms/auth/CurrentUser.java index b43bf98b5..faf5555d0 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/CurrentUser.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/CurrentUser.java @@ -6,6 +6,7 @@ import java.security.PrivilegedAction; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.HashSet; +import java.util.Iterator; import java.util.Locale; import java.util.Set; import java.util.UUID; @@ -125,7 +126,12 @@ public final class CurrentUser { public static CmsSession getCmsSession() { Subject subject = currentSubject(); - CmsSessionId cmsSessionId = subject.getPrivateCredentials(CmsSessionId.class).iterator().next(); + Iterator it = subject.getPrivateCredentials(CmsSessionId.class).iterator(); + if (!it.hasNext()) + throw new IllegalStateException("No CMS session id available for " + subject); + CmsSessionId cmsSessionId = it.next(); + if (it.hasNext()) + throw new IllegalStateException("More than one CMS session id available for " + subject); return CmsContextImpl.getCmsContext().getCmsSessionByUuid(cmsSessionId.getUuid()); } @@ -165,8 +171,8 @@ public final class CurrentUser { } /* - * PREPARE EVOLUTION OF JAVA APIs INTRODUCED IN JDK 18 - * The following static methods will be added to Subject + * PREPARE EVOLUTION OF JAVA APIs INTRODUCED IN JDK 18 The following static + * methods will be added to Subject */ public Subject current() { return currentSubject(); diff --git a/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsUserManagerImpl.java b/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsUserManagerImpl.java index 782487a9a..84562ebd1 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsUserManagerImpl.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/auth/CmsUserManagerImpl.java @@ -17,6 +17,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import java.util.UUID; import javax.naming.InvalidNameException; @@ -243,6 +244,12 @@ public class CmsUserManagerImpl implements CmsUserManager { return dns; } + public Set getUserDirectories() { + TreeSet res = new TreeSet<>((o1, o2) -> o1.getBasePath().compareTo(o2.getBasePath())); + res.addAll(userDirectories.keySet()); + return res; + } + public String buildDistinguishedName(String localId, String baseDn, int type) { Map dns = getKnownBaseDns(true); Dictionary props = UserAdminConf.uriAsProperties(dns.get(baseDn)); diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/InitUtils.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/InitUtils.java index 1ca5e4a55..821808017 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/InitUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/InitUtils.java @@ -182,7 +182,8 @@ public class InitUtils { // TODO downgrade security level } for (String userAdminUri : userAdminUris.split(" ")) - uris.add(userAdminUri); + if (!userAdminUri.trim().equals("")) + uris.add(userAdminUri); // Interprets URIs for (String uri : uris) { diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java b/org.argeo.util/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java index 19cd06886..716ddb5ed 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java @@ -124,7 +124,7 @@ abstract class AbstractUserDirectory implements UserAdmin, UserDirectory { protected abstract DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException; - protected abstract List doGetRoles(Filter f); + protected abstract List doGetRoles(LdapName searchBase, Filter f, boolean deep); protected abstract AbstractUserDirectory scope(User user); @@ -240,9 +240,31 @@ abstract class AbstractUserDirectory implements UserAdmin, UserDirectory { @Override public Role[] getRoles(String filter) throws InvalidSyntaxException { +// UserDirectoryWorkingCopy wc = getWorkingCopy(); +// Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null; +// List res = doGetRoles(getBaseDn(), f, true); +// if (wc != null) { +// for (Iterator it = res.iterator(); it.hasNext();) { +// DirectoryUser user = it.next(); +// LdapName dn = user.getDn(); +// if (wc.getDeletedUsers().containsKey(dn)) +// it.remove(); +// } +// for (DirectoryUser user : wc.getNewUsers().values()) { +// if (f == null || f.match(user.getProperties())) +// res.add(user); +// } +// // no need to check modified users, +// // since doGetRoles was already based on the modified attributes +// } + List res = getRoles(getBaseDn(), filter, true); + return res.toArray(new Role[res.size()]); + } + + List getRoles(LdapName searchBase, String filter, boolean deep) throws InvalidSyntaxException { UserDirectoryWorkingCopy wc = getWorkingCopy(); Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null; - List res = doGetRoles(f); + List res = doGetRoles(searchBase, f, deep); if (wc != null) { for (Iterator it = res.iterator(); it.hasNext();) { DirectoryUser user = it.next(); @@ -257,7 +279,22 @@ abstract class AbstractUserDirectory implements UserAdmin, UserDirectory { // no need to check modified users, // since doGetRoles was already based on the modified attributes } - return res.toArray(new Role[res.size()]); + + // if non deep we also search users and groups + if (!deep) { + try { + if (!(searchBase.endsWith(new LdapName(getUserBase())) + || searchBase.endsWith(new LdapName(getGroupBase())))) { + LdapName usersBase = (LdapName) ((LdapName) searchBase.clone()).add(getUserBase()); + res.addAll(getRoles(usersBase, filter, false)); + LdapName groupsBase = (LdapName) ((LdapName) searchBase.clone()).add(getGroupBase()); + res.addAll(getRoles(groupsBase, filter, false)); + } + } catch (InvalidNameException e) { + throw new IllegalStateException("Cannot search users and groups", e); + } + } + return res; } @Override @@ -282,7 +319,7 @@ abstract class AbstractUserDirectory implements UserAdmin, UserDirectory { protected void doGetUser(String key, String value, List collectedUsers) { try { Filter f = FrameworkUtil.createFilter("(" + key + "=" + value + ")"); - List users = doGetRoles(f); + List users = doGetRoles(getBaseDn(), f, true); collectedUsers.addAll(users); } catch (InvalidSyntaxException e) { throw new IllegalArgumentException("Cannot get user with " + key + "=" + value, e); @@ -394,6 +431,40 @@ abstract class AbstractUserDirectory implements UserAdmin, UserDirectory { } + /* + * HIERARCHY + */ + @Override + public int getHierarchyChildCount() { + return 0; + } + + @Override + public HierarchyUnit getHierarchyChild(int i) { + throw new IllegalArgumentException("No child hierarchy unit available"); + } + + @Override + public int getHierarchyUnitType() { + return 0; + } + + @Override + public String getHierarchyUnitName() { + String name = baseDn.getRdn(baseDn.size() - 1).getValue().toString(); + // TODO check ou, o, etc. + return name; + } + + @Override + public List getRoles(String filter, boolean deep) { + try { + return getRoles(getBaseDn(), filter, deep); + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException("Cannot filter " + filter + " " + getBaseDn(), e); + } + } + // GETTERS protected String getMemberAttributeId() { return memberAttributeId; @@ -512,6 +583,16 @@ abstract class AbstractUserDirectory implements UserAdmin, UserDirectory { return scoped; } + @Override + public int hashCode() { + return baseDn.hashCode(); + } + + @Override + public String toString() { + return "User Directory " + baseDn.toString(); + } + /* * STATIC UTILITIES */ diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/HierarchyUnit.java b/org.argeo.util/src/org/argeo/osgi/useradmin/HierarchyUnit.java new file mode 100644 index 000000000..c01ea15f4 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/HierarchyUnit.java @@ -0,0 +1,24 @@ +package org.argeo.osgi.useradmin; + +import java.util.List; + +import org.osgi.service.useradmin.Role; + +/** A unit within the high-level organisational structure of a directory. */ +public interface HierarchyUnit { + final static int UNKOWN = 0; + final static int ORGANIZATION = 1; + final static int OU = 2; + + String getHierarchyUnitName(); + + int getHierarchyChildCount(); + + HierarchyUnit getHierarchyChild(int i); + + int getHierarchyUnitType(); + + String getBasePath(); + + List getRoles(String filter, boolean deep); +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdapUserAdmin.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdapUserAdmin.java index f8396085b..623ca2a92 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdapUserAdmin.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdapUserAdmin.java @@ -95,16 +95,17 @@ public class LdapUserAdmin extends AbstractUserDirectory { } @Override - protected List doGetRoles(Filter f) { + protected List doGetRoles(LdapName searchBase, Filter f, boolean deep) { ArrayList res = new ArrayList(); try { String searchFilter = f != null ? f.toString() : "(|(" + objectClass + "=" + getUserObjectClass() + ")(" + objectClass + "=" + getGroupObjectClass() + "))"; SearchControls searchControls = new SearchControls(); - searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + // FIXME make one level consistent with deep + searchControls.setSearchScope(deep ? SearchControls.SUBTREE_SCOPE : SearchControls.ONELEVEL_SCOPE); - LdapName searchBase = getBaseDn(); + // LdapName searchBase = getBaseDn(); NamingEnumeration results = ldapConnection.search(searchBase, searchFilter, searchControls); results: while (results.hasMoreElements()) { diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifHierarchyUnit.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifHierarchyUnit.java new file mode 100644 index 000000000..5cf52b9a2 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifHierarchyUnit.java @@ -0,0 +1,91 @@ +package org.argeo.osgi.useradmin; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; + +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.service.useradmin.Role; + +/** LDIF/LDAP based implementation of {@link HierarchyUnit}. */ +class LdifHierarchyUnit implements HierarchyUnit { + private final AbstractUserDirectory directory; + + private final LdapName dn; + private final int type; + private final Attributes attributes; + + HierarchyUnit parent; + List children = new ArrayList<>(); + + LdifHierarchyUnit(AbstractUserDirectory directory, LdapName dn, int type, Attributes attributes) { + Objects.requireNonNull(directory); + Objects.requireNonNull(dn); + + this.directory = directory; + this.dn = dn; + this.type = type; + this.attributes = attributes; + } + + @Override + public int getHierarchyChildCount() { + return children.size(); + } + + @Override + public HierarchyUnit getHierarchyChild(int i) { + return children.get(i); + } + + @Override + public int getHierarchyUnitType() { + return type; + } + + @Override + public String getHierarchyUnitName() { + String name = dn.getRdn(dn.size() - 1).getValue().toString(); + // TODO check ou, o, etc. + return name; + } + + public Attributes getAttributes() { + return attributes; + } + + @Override + public String getBasePath() { + return dn.toString(); + } + + @Override + public List getRoles(String filter, boolean deep) { + try { + return directory.getRoles(dn, filter, deep); + } catch (InvalidSyntaxException e) { + throw new IllegalArgumentException("Cannot filter " + filter + " " + dn, e); + } + } + + @Override + public int hashCode() { + return dn.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof LdifHierarchyUnit)) + return false; + return ((LdifHierarchyUnit) obj).dn.equals(dn); + } + + @Override + public String toString() { + return "Hierarchy Unit " + dn.toString(); + } + +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUserAdmin.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUserAdmin.java index 8b1206a72..f9163d7e2 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUserAdmin.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUserAdmin.java @@ -16,6 +16,7 @@ import java.util.Dictionary; import java.util.HashSet; import java.util.Hashtable; import java.util.List; +import java.util.Objects; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; @@ -25,6 +26,7 @@ import javax.naming.NamingEnumeration; import javax.naming.directory.Attributes; import javax.naming.ldap.LdapName; +import org.argeo.util.naming.LdapObjs; import org.argeo.util.naming.LdifParser; import org.argeo.util.naming.LdifWriter; import org.osgi.framework.Filter; @@ -33,8 +35,11 @@ import org.osgi.service.useradmin.User; /** A user admin based on a LDIF files. */ public class LdifUserAdmin extends AbstractUserDirectory { - private SortedMap users = new TreeMap(); - private SortedMap groups = new TreeMap(); + private SortedMap users = new TreeMap<>(); + private SortedMap groups = new TreeMap<>(); + + private SortedMap hierarchy = new TreeMap<>(); + private List rootHierarchyUnits = new ArrayList<>(); public LdifUserAdmin(String uri, String baseDn) { this(fromUri(uri, baseDn), false); @@ -113,6 +118,8 @@ public class LdifUserAdmin extends AbstractUserDirectory { 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 : groups.keySet()) ldifWriter.writeEntry(name, groups.get(name).getAttributes()); for (LdapName name : users.keySet()) @@ -126,6 +133,7 @@ public class LdifUserAdmin extends AbstractUserDirectory { try { users.clear(); groups.clear(); + hierarchy.clear(); LdifParser ldifParser = new LdifParser(); SortedMap allEntries = ldifParser.read(in); @@ -153,9 +161,34 @@ public class LdifUserAdmin extends AbstractUserDirectory { } else if (objectClass.toLowerCase().equals(getGroupObjectClass().toLowerCase())) { groups.put(key, new LdifGroup(this, key, attributes)); break objectClasses; + } else if (objectClass.equalsIgnoreCase(LdapObjs.organization.name())) { + // we only consider organizations which are not groups + hierarchy.put(key, new LdifHierarchyUnit(this, key, HierarchyUnit.ORGANIZATION, attributes)); + break objectClasses; + } else if (objectClass.equalsIgnoreCase(LdapObjs.organizationalUnit.name())) { + String name = key.getRdn(key.size() - 1).toString(); + if (getUserBase().equalsIgnoreCase(name) || getGroupBase().equalsIgnoreCase(name)) + break objectClasses; // skip + // TODO skip if it does not contain groups or users + hierarchy.put(key, new LdifHierarchyUnit(this, key, HierarchyUnit.OU, attributes)); + break objectClasses; } } } + + // link hierarchy + hierachyUnits: for (LdapName dn : hierarchy.keySet()) { + LdifHierarchyUnit unit = hierarchy.get(dn); + LdapName parentDn = (LdapName) dn.getPrefix(dn.size() - 1); + LdifHierarchyUnit parent = hierarchy.get(parentDn); + if (parent == null) { + rootHierarchyUnits.add(unit); + unit.parent = this; + continue hierachyUnits; + } + parent.children.add(unit); + unit.parent = parent; + } } catch (Exception e) { throw new UserDirectoryException("Cannot load user admin service from LDIF", e); } @@ -168,6 +201,10 @@ public class LdifUserAdmin extends AbstractUserDirectory { groups = null; } + /* + * USER ADMIN + */ + @Override protected DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException { if (groups.containsKey(key)) @@ -182,21 +219,35 @@ public class LdifUserAdmin extends AbstractUserDirectory { return users.containsKey(dn) || groups.containsKey(dn); } - protected List doGetRoles(Filter f) { + @Override + protected List doGetRoles(LdapName searchBase, Filter f, boolean deep) { + Objects.requireNonNull(searchBase); ArrayList res = new ArrayList(); - if (f == null) { + if (f == null && deep && getBaseDn().equals(searchBase)) { res.addAll(users.values()); res.addAll(groups.values()); } else { - for (DirectoryUser user : users.values()) { - if (f.match(user.getProperties())) + filterRoles(users, searchBase, f, deep, res); + filterRoles(groups, searchBase, f, deep, res); + } + return res; + } + + private void filterRoles(SortedMap map, LdapName searchBase, Filter f, + boolean deep, List res) { + // TODO reduce map with search base ? + roles: for (DirectoryUser user : map.values()) { + LdapName dn = user.getDn(); + if (dn.startsWith(searchBase)) { + if (!deep && dn.size() != (searchBase.size() + 1)) + continue roles; + if (f == null) + res.add(user); + else if (f.match(user.getProperties())) res.add(user); } - for (DirectoryUser group : groups.values()) - if (f.match(group.getProperties())) - res.add(group); } - return res; + } @Override @@ -257,4 +308,18 @@ public class LdifUserAdmin extends AbstractUserDirectory { init(); } + @Override + public int getHierarchyChildCount() { + return rootHierarchyUnits.size(); + } + + @Override + public HierarchyUnit getHierarchyChild(int i) { + return rootHierarchyUnits.get(i); + } + + /* + * HIERARCHY + */ + } diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java b/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java index dd16e1a3b..68f2eabcd 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java @@ -51,7 +51,7 @@ public class OsUserDirectory extends AbstractUserDirectory { } @Override - protected List doGetRoles(Filter f) { + protected List doGetRoles(LdapName searchBase, Filter f, boolean deep) { List res = new ArrayList<>(); if (f == null || f.match(osUser.getProperties())) res.add(osUser); diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectory.java b/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectory.java index 7d773a9e7..5a69f1a16 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectory.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectory.java @@ -5,7 +5,7 @@ import java.util.Optional; import org.argeo.osgi.transaction.WorkControl; /** Information about a user directory. */ -public interface UserDirectory { +public interface UserDirectory extends HierarchyUnit { /** * The base of the hierarchy defined by this directory. This could typically be * an LDAP base DN. @@ -31,7 +31,7 @@ public interface UserDirectory { String getGroupBase(); Optional getRealm(); - + @Deprecated void setTransactionControl(WorkControl transactionControl); } diff --git a/rap/org.argeo.cms.ui.rap/src/org/argeo/cms/web/CmsWebEntryPoint.java b/rap/org.argeo.cms.ui.rap/src/org/argeo/cms/web/CmsWebEntryPoint.java index 2eb783527..2236870f6 100644 --- a/rap/org.argeo.cms.ui.rap/src/org/argeo/cms/web/CmsWebEntryPoint.java +++ b/rap/org.argeo.cms.ui.rap/src/org/argeo/cms/web/CmsWebEntryPoint.java @@ -269,6 +269,8 @@ public class CmsWebEntryPoint implements EntryPoint, CmsView, BrowserNavigationL @Override public CmsSession getCmsSession() { CmsSession cmsSession = cmsWebApp.getCmsApp().getCmsContext().getCmsSession(getSubject()); + if (cmsSession == null) + throw new IllegalStateException("No CMS session available for " + getSubject()); return cmsSession; } -- 2.30.2