From 4c7e1885b8bf3c93fa0919ace122e3f289a925ea Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Fri, 17 Jun 2022 12:12:25 +0200 Subject: [PATCH] Introduce directory content provider --- .../argeo/cms/swt/acr/AcrContentTreeView.java | 129 ++++++++-------- .../swt/acr/SwtContentHierarchicalPart.java | 55 +++++++ .../cms/swt/widgets/SwtHierarchicalPart.java | 4 + .../src/org/argeo/cms/jcr/acr/JcrContent.java | 73 +++++++-- .../src/org/argeo/api/acr/Content.java | 7 +- .../src/org/argeo/api/acr/CrName.java | 5 + .../src/org/argeo/api/acr/NamespaceUtils.java | 14 ++ .../OSGI-INF/acrContentRepository.xml | 1 + .../src/org/argeo/cms/CmsUserManager.java | 2 + .../org/argeo/cms/acr/AbstractContent.java | 66 +++++++- .../cms/acr/AbstractContentRepository.java | 12 +- .../argeo/cms/acr/CmsContentRepository.java | 1 + .../src/org/argeo/cms/acr/ContentUtils.java | 49 +++++- .../src/org/argeo/cms/acr/TypesManager.java | 9 +- .../directory/DirectoryContentProvider.java | 145 ++++++++++++++++++ .../acr/directory/HierarchyUnitContent.java | 78 ++++++++++ .../argeo/cms/acr/directory/RoleContent.java | 125 +++++++++++++++ .../src/org/argeo/cms/acr/fs/FsContent.java | 37 +++-- .../src/org/argeo/cms/acr/xml/DomContent.java | 30 ++-- .../cms/internal/auth/CmsUserManagerImpl.java | 16 ++ .../runtime/DeployedContentRepository.java | 17 +- .../src/org/argeo/cms/runtime/StaticCms.java | 19 ++- .../osgi/useradmin/AbstractUserDirectory.java | 17 +- .../osgi/useradmin/AggregatingUserAdmin.java | 6 + .../argeo/osgi/useradmin/HierarchyUnit.java | 5 + .../argeo/osgi/useradmin/LdapNameUtils.java | 58 +++++++ .../argeo/osgi/useradmin/LdapUserAdmin.java | 7 + .../osgi/useradmin/LdifHierarchyUnit.java | 8 +- .../org/argeo/osgi/useradmin/LdifUser.java | 30 ++-- .../argeo/osgi/useradmin/LdifUserAdmin.java | 22 ++- .../argeo/osgi/useradmin/UserDirectory.java | 7 +- 31 files changed, 914 insertions(+), 140 deletions(-) create mode 100644 eclipse/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/SwtContentHierarchicalPart.java create mode 100644 org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java create mode 100644 org.argeo.cms/src/org/argeo/cms/acr/directory/HierarchyUnitContent.java create mode 100644 org.argeo.cms/src/org/argeo/cms/acr/directory/RoleContent.java create mode 100644 org.argeo.util/src/org/argeo/osgi/useradmin/LdapNameUtils.java diff --git a/eclipse/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/AcrContentTreeView.java b/eclipse/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/AcrContentTreeView.java index 4deef49c1..98fabf060 100644 --- a/eclipse/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/AcrContentTreeView.java +++ b/eclipse/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/AcrContentTreeView.java @@ -1,28 +1,21 @@ package org.argeo.cms.swt.acr; -import java.nio.file.Path; -import java.nio.file.Paths; - import javax.xml.namespace.QName; import org.argeo.api.acr.Content; -import org.argeo.cms.acr.fs.FsContentProvider; import org.argeo.cms.swt.CmsSwtUtils; import org.eclipse.swt.SWT; -import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; -import org.eclipse.swt.widgets.Display; -import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.TableItem; -import org.eclipse.swt.widgets.Tree; -import org.eclipse.swt.widgets.TreeItem; public class AcrContentTreeView extends Composite { - private Tree tree; + private static final long serialVersionUID = -3707881216246077323L; + private SwtContentHierarchicalPart hPart; +// private Tree tree; private Table table; private Content rootContent; @@ -36,44 +29,52 @@ public class AcrContentTreeView extends Composite { initTree(); GridData treeGd = CmsSwtUtils.fillHeight(); treeGd.widthHint = 300; - tree.setLayoutData(treeGd); + hPart.getArea().setLayoutData(treeGd); initTable(); table.setLayoutData(CmsSwtUtils.fillAll()); } protected void initTree() { - tree = new Tree(this, 0); - for (Content c : rootContent) { - TreeItem root = new TreeItem(tree, 0); - root.setText(c.getName().toString()); - root.setData(c); - new TreeItem(root, 0); - } - tree.addListener(SWT.Expand, event -> { - final TreeItem root = (TreeItem) event.item; - TreeItem[] items = root.getItems(); - for (TreeItem item : items) { - if (item.getData() != null) - return; - item.dispose(); - } - Content content = (Content) root.getData(); - for (Content c : content) { - TreeItem item = new TreeItem(root, 0); - item.setText(c.getName().toString()); - item.setData(c); - boolean hasChildren = true; - if (hasChildren) { - new TreeItem(item, 0); - } - } - }); - tree.addListener(SWT.Selection, event -> { - TreeItem item = (TreeItem) event.item; - selected = (Content) item.getData(); + hPart = new SwtContentHierarchicalPart(this, getStyle()); + hPart.setInput(rootContent); + hPart.onSelected((o) -> { + Content content = (Content) o; + selected = content; refreshTable(); + }); +// tree = new Tree(this, 0); +// for (Content c : rootContent) { +// TreeItem root = new TreeItem(tree, 0); +// root.setText(c.getName().toString()); +// root.setData(c); +// new TreeItem(root, 0); +// } +// tree.addListener(SWT.Expand, event -> { +// final TreeItem root = (TreeItem) event.item; +// TreeItem[] items = root.getItems(); +// for (TreeItem item : items) { +// if (item.getData() != null) +// return; +// item.dispose(); +// } +// Content content = (Content) root.getData(); +// for (Content c : content) { +// TreeItem item = new TreeItem(root, 0); +// item.setText(c.getName().toString()); +// item.setData(c); +// boolean hasChildren = true; +// if (hasChildren) { +// new TreeItem(item, 0); +// } +// } +// }); +// tree.addListener(SWT.Selection, event -> { +// TreeItem item = (TreeItem) event.item; +// selected = (Content) item.getData(); +// refreshTable(); +// }); } protected void initTable() { @@ -103,28 +104,28 @@ public class AcrContentTreeView extends Composite { table.getColumn(1).pack(); } - public static void main(String[] args) { - Path basePath; - if (args.length > 0) { - basePath = Paths.get(args[0]); - } else { - basePath = Paths.get(System.getProperty("user.home")); - } - - final Display display = new Display(); - final Shell shell = new Shell(display); - shell.setText(basePath.toString()); - shell.setLayout(new FillLayout()); - - FsContentProvider contentSession = new FsContentProvider("/", basePath); -// GcrContentTreeView treeView = new GcrContentTreeView(shell, 0, contentSession.get("/")); - - shell.setSize(shell.computeSize(800, 600)); - shell.open(); - while (!shell.isDisposed()) { - if (!display.readAndDispatch()) - display.sleep(); - } - display.dispose(); - } +// public static void main(String[] args) { +// Path basePath; +// if (args.length > 0) { +// basePath = Paths.get(args[0]); +// } else { +// basePath = Paths.get(System.getProperty("user.home")); +// } +// +// final Display display = new Display(); +// final Shell shell = new Shell(display); +// shell.setText(basePath.toString()); +// shell.setLayout(new FillLayout()); +// +// FsContentProvider contentSession = new FsContentProvider("/", basePath); +//// GcrContentTreeView treeView = new GcrContentTreeView(shell, 0, contentSession.get("/")); +// +// shell.setSize(shell.computeSize(800, 600)); +// shell.open(); +// while (!shell.isDisposed()) { +// if (!display.readAndDispatch()) +// display.sleep(); +// } +// display.dispose(); +// } } diff --git a/eclipse/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/SwtContentHierarchicalPart.java b/eclipse/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/SwtContentHierarchicalPart.java new file mode 100644 index 000000000..230cca4c8 --- /dev/null +++ b/eclipse/org.argeo.cms.swt/src/org/argeo/cms/swt/acr/SwtContentHierarchicalPart.java @@ -0,0 +1,55 @@ +package org.argeo.cms.swt.acr; + +import java.util.Iterator; + +import org.argeo.api.acr.Content; +import org.argeo.cms.swt.widgets.SwtHierarchicalPart; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.TreeItem; + +public class SwtContentHierarchicalPart extends SwtHierarchicalPart { + + public SwtContentHierarchicalPart(Composite parent, int style) { + super(parent, style); + } + + public Content getContent() { + return (Content) getInput(); + } + + @Override + protected void refreshRootItem(TreeItem item) { + refreshItem(null, item); + } + + @Override + protected void refreshItem(TreeItem parentItem, TreeItem item) { + int index = getTree().indexOf(item); + Content parentContent = parentItem == null ? getContent() : (Content) parentItem.getData(); + Content content = null; + int count = 0; + children: for (Content c : parentContent) { + if (count == index) { + content = c; + break children; + } + count++; + } + item.setData(content); + item.setText(content.getName().toString()); + item.setItemCount(getChildrenCount(content)); + } + + @Override + protected int getRootItemCount() { + return getChildrenCount(getContent()); + } + + static int getChildrenCount(Content content) { + int count = 0; + for (Iterator it = content.iterator(); it.hasNext();it.next()) { + count++; + } + return count; + } +} 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 08c85b165..a4ca061e6 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 @@ -95,4 +95,8 @@ public class SwtHierarchicalPart implements HierarchicalPart { this.onAction = onAction; } + public Composite getArea() { + return area; + } + } diff --git a/jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContent.java b/jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContent.java index 116e45c7b..a7a9ecce0 100644 --- a/jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContent.java +++ b/jcr/org.argeo.cms.jcr/src/org/argeo/cms/jcr/acr/JcrContent.java @@ -12,6 +12,7 @@ import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.ForkJoinPool; import javax.jcr.Node; @@ -42,13 +43,12 @@ public class JcrContent extends AbstractContent { // private Node jcrNode; private JcrContentProvider provider; - private ProvidedSession session; private String jcrWorkspace; private String jcrPath; protected JcrContent(ProvidedSession session, JcrContentProvider provider, String jcrWorkspace, String jcrPath) { - this.session = session; + super(session); this.provider = provider; this.jcrWorkspace = jcrWorkspace; this.jcrPath = jcrPath; @@ -103,7 +103,7 @@ public class JcrContent extends AbstractContent { public Node getJcrNode() { try { // TODO caching? - return provider.getJcrSession(session, jcrWorkspace).getNode(jcrPath); + return provider.getJcrSession(getSession(), jcrWorkspace).getNode(jcrPath); } catch (RepositoryException e) { throw new JcrException("Cannot retrieve " + jcrPath + " from workspace " + jcrWorkspace, e); } @@ -165,7 +165,7 @@ public class JcrContent extends AbstractContent { @Override public Content next() { - current = new JcrContent(session, provider, jcrWorkspace, Jcr.getPath(nodeIterator.nextNode())); + current = new JcrContent(getSession(), provider, jcrWorkspace, Jcr.getPath(nodeIterator.nextNode())); return current; } @@ -182,7 +182,7 @@ public class JcrContent extends AbstractContent { public Content getParent() { if (Jcr.isRoot(getJcrNode())) // root return null; - return new JcrContent(session, provider, jcrWorkspace, Jcr.getParentPath(getJcrNode())); + return new JcrContent(getSession(), provider, jcrWorkspace, Jcr.getParentPath(getJcrNode())); } @Override @@ -224,7 +224,7 @@ public class JcrContent extends AbstractContent { boolean exists() { try { - return provider.getJcrSession(session, jcrWorkspace).itemExists(jcrPath); + return provider.getJcrSession(getSession(), jcrWorkspace).itemExists(jcrPath); } catch (RepositoryException e) { throw new JcrException("Cannot check whether " + jcrPath + " exists", e); } @@ -241,7 +241,7 @@ public class JcrContent extends AbstractContent { ForkJoinPool.commonPool().execute(() -> { try (PipedOutputStream out = new PipedOutputStream(in)) { - provider.getJcrSession(session, jcrWorkspace).exportDocumentView(jcrPath, out, true, false); + provider.getJcrSession(getSession(), jcrWorkspace).exportDocumentView(jcrPath, out, true, false); out.flush(); } catch (IOException | RepositoryException e) { throw new RuntimeException("Cannot export " + jcrPath + " in workspace " + jcrWorkspace, e); @@ -273,11 +273,6 @@ public class JcrContent extends AbstractContent { return super.open(clss); } - @Override - public ProvidedSession getSession() { - return session; - } - @Override public ContentProvider getProvider() { return provider; @@ -292,6 +287,60 @@ public class JcrContent extends AbstractContent { } } + /* + * TYPING + */ + @Override + public List getTypes() { + try { +// Node node = getJcrNode(); +// List res = new ArrayList<>(); +// res.add(nodeTypeToQName(node.getPrimaryNodeType())); +// for (NodeType mixin : node.getMixinNodeTypes()) { +// res.add(nodeTypeToQName(mixin)); +// } +// return res; + Node context = getJcrNode(); + + List res = new ArrayList<>(); + // primary node type + NodeType primaryType = context.getPrimaryNodeType(); + res.add(nodeTypeToQName(primaryType)); + + Set secondaryTypes = new TreeSet<>(); + for (NodeType mixinType : context.getMixinNodeTypes()) { + secondaryTypes.add(nodeTypeToQName(mixinType)); + } + for (NodeType superType : primaryType.getDeclaredSupertypes()) { + secondaryTypes.add(nodeTypeToQName(superType)); + } + // mixins + for (NodeType mixinType : context.getMixinNodeTypes()) { + for (NodeType superType : mixinType.getDeclaredSupertypes()) { + secondaryTypes.add(nodeTypeToQName(superType)); + } + } +// // entity type +// if (context.isNodeType(EntityType.entity.get())) { +// if (context.hasProperty(EntityNames.ENTITY_TYPE)) { +// String entityTypeName = context.getProperty(EntityNames.ENTITY_TYPE).getString(); +// if (byType.containsKey(entityTypeName)) { +// types.add(entityTypeName); +// } +// } +// } + res.addAll(secondaryTypes); + return res; + } catch (RepositoryException e) { + throw new JcrException("Cannot list node types from " + getJcrNode(), e); + } + } + + private QName nodeTypeToQName(NodeType nodeType) { + String name = nodeType.getName(); + return QName.valueOf(name); + } + /* * STATIC UTLITIES */ diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/Content.java b/org.argeo.api.acr/src/org/argeo/api/acr/Content.java index c2202b0fa..cd03b16a8 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/Content.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/Content.java @@ -89,10 +89,15 @@ public interface Content extends Iterable, Map { void remove(); + /* + * TYPING + */ + List getTypes(); + /* * DEFAULT METHODS */ - default A adapt(Class clss) throws IllegalArgumentException { + default A adapt(Class clss) { throw new UnsupportedOperationException("Cannot adapt content " + this + " to " + clss.getName()); } diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/CrName.java b/org.argeo.api.acr/src/org/argeo/api/acr/CrName.java index 1b29b3661..c772daca3 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/CrName.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/CrName.java @@ -39,6 +39,11 @@ public enum CrName implements ContentNameSupplier { // public final static String CR_POSIX_NAMESPACE_URI = CR_NAMESPACE_URI + "/posix"; public final static String CR_DEFAULT_PREFIX = "cr"; + + public final static String LDAP_NAMESPACE_URI = "http://argeo.org/ns/ldap"; + public final static String LDAP_DEFAULT_PREFIX = "ldap"; + + private final ContentName value; CrName() { diff --git a/org.argeo.api.acr/src/org/argeo/api/acr/NamespaceUtils.java b/org.argeo.api.acr/src/org/argeo/api/acr/NamespaceUtils.java index a1b4062aa..9b5034b55 100644 --- a/org.argeo.api.acr/src/org/argeo/api/acr/NamespaceUtils.java +++ b/org.argeo.api.acr/src/org/argeo/api/acr/NamespaceUtils.java @@ -1,6 +1,7 @@ package org.argeo.api.acr; import java.util.Collections; +import java.util.Comparator; import java.util.Iterator; import java.util.Objects; import java.util.Set; @@ -39,6 +40,19 @@ public class NamespaceUtils { return prefix + ":" + name.getLocalPart(); } + public final static Comparator QNAME_COMPARATOR = new Comparator() { + + @Override + public int compare(QName qn1, QName qn2) { + if (Objects.equals(qn1.getNamespaceURI(), qn2.getNamespaceURI())) {// same namespace + return qn1.getLocalPart().compareTo(qn2.getLocalPart()); + } else { + return qn1.getNamespaceURI().compareTo(qn2.getNamespaceURI()); + } + } + + }; + /** singleton */ private NamespaceUtils() { } diff --git a/org.argeo.cms/OSGI-INF/acrContentRepository.xml b/org.argeo.cms/OSGI-INF/acrContentRepository.xml index d0d31db9c..befc95889 100644 --- a/org.argeo.cms/OSGI-INF/acrContentRepository.xml +++ b/org.argeo.cms/OSGI-INF/acrContentRepository.xml @@ -8,4 +8,5 @@ + diff --git a/org.argeo.cms/src/org/argeo/cms/CmsUserManager.java b/org.argeo.cms/src/org/argeo/cms/CmsUserManager.java index c51d21caa..5256dbfed 100644 --- a/org.argeo.cms/src/org/argeo/cms/CmsUserManager.java +++ b/org.argeo.cms/src/org/argeo/cms/CmsUserManager.java @@ -80,6 +80,8 @@ public interface CmsUserManager { void expireAuthToken(String token); void expireAuthTokens(Subject subject); + + UserDirectory getUserDirectory(User user); // User createUserFromPerson(Node person); diff --git a/org.argeo.cms/src/org/argeo/cms/acr/AbstractContent.java b/org.argeo.cms/src/org/argeo/cms/acr/AbstractContent.java index fa8062f7c..18c6724ac 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/AbstractContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/AbstractContent.java @@ -15,16 +15,22 @@ import javax.xml.namespace.QName; import org.argeo.api.acr.Content; import org.argeo.api.acr.CrName; import org.argeo.api.acr.spi.ProvidedContent; +import org.argeo.api.acr.spi.ProvidedSession; /** Partial reference implementation of a {@link ProvidedContent}. */ public abstract class AbstractContent extends AbstractMap implements ProvidedContent { + private final ProvidedSession session; + + public AbstractContent(ProvidedSession session) { + this.session = session; + } /* * ATTRIBUTES OPERATIONS */ - protected abstract Iterable keys(); - - protected abstract void removeAttr(QName key); +// protected abstract Iterable keys(); +// +// protected abstract void removeAttr(QName key); @Override public Set> entrySet() { @@ -106,12 +112,30 @@ public abstract class AbstractContent extends AbstractMap impleme collectAncestors(ancestors, this); return ancestors.size(); } - + @Override public String getSessionLocalId() { return getPath(); } + /* + * SESSION + */ + + @Override + public ProvidedSession getSession() { + return session; + } + + /* + * TYPING + */ + + @Override + public List getTypes() { + return new ArrayList<>(); + } + /* * UTILITIES */ @@ -125,6 +149,40 @@ public abstract class AbstractContent extends AbstractMap impleme // return "content " + getPath(); // } + /* + * DEFAULTS + */ + // - no children + // - no attributes + // - cannot be modified + @Override + public Iterator iterator() { + return Collections.emptyIterator(); + } + + @Override + public Content add(QName name, QName... classes) { + throw new UnsupportedOperationException("Content cannot be added."); + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Content cannot be removed."); + } + + protected Iterable keys() { + return Collections.emptySet(); + } + + @Override + public Optional get(QName key, Class clss) { + return null; + } + + protected void removeAttr(QName key) { + throw new UnsupportedOperationException("Attributes cannot be removed."); + } + /* * SUB CLASSES */ diff --git a/org.argeo.cms/src/org/argeo/cms/acr/AbstractContentRepository.java b/org.argeo.cms/src/org/argeo/cms/acr/AbstractContentRepository.java index cfffea027..1481b3a40 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/AbstractContentRepository.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/AbstractContentRepository.java @@ -5,6 +5,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.util.HashSet; import java.util.Set; import javax.xml.namespace.QName; @@ -40,6 +41,8 @@ public abstract class AbstractContentRepository implements ProvidedRepository { private CmsContentSession systemSession; + private Set providersToAdd = new HashSet<>(); + // utilities /** Should be used only to copy source and results. */ private TransformerFactory identityTransformerFactory = TransformerFactory.newInstance(); @@ -76,7 +79,10 @@ public abstract class AbstractContentRepository implements ProvidedRepository { */ public void addProvider(ContentProvider provider) { - mountManager.addStructuralContentProvider(provider); + if (mountManager == null) + providersToAdd.add(provider); + else + mountManager.addStructuralContentProvider(provider); } public void registerTypes(String prefix, String namespaceURI, String schemaSystemId) { @@ -128,6 +134,10 @@ public abstract class AbstractContentRepository implements ProvidedRepository { throw new IllegalStateException("Cannot init ACR root " + path, e); } + // add content providers already notified + for (ContentProvider contentProvider : providersToAdd) + addProvider(contentProvider); + providersToAdd.clear(); } public void writeDom(Document document, OutputStream out) throws IOException { diff --git a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentRepository.java b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentRepository.java index 46222ce1a..3e01aee8b 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/CmsContentRepository.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/CmsContentRepository.java @@ -22,6 +22,7 @@ import org.argeo.cms.internal.runtime.CmsContextImpl; */ public class CmsContentRepository extends AbstractContentRepository { public final static String RUN_BASE = "/run"; + public final static String DIRECTORY_BASE = "/directory"; private Map userSessions = Collections.synchronizedMap(new HashMap<>()); diff --git a/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java b/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java index 5609cf777..272a5c5a1 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java @@ -1,12 +1,13 @@ package org.argeo.cms.acr; import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; import java.util.function.BiConsumer; import javax.xml.namespace.QName; import org.argeo.api.acr.Content; -import org.argeo.api.cms.CmsSession; /** Utilities and routines around {@link Content}. */ public class ContentUtils { @@ -50,18 +51,56 @@ public class ContentUtils { // return t instanceof String; // } + public static final char SLASH = '/'; + public static final String ROOT_SLASH = "" + SLASH; + /** * Split a path (with '/' separator) in an array of length 2, the first part * being the parent path (which could be either absolute or relative), the - * second one being the last segment, (guaranteed to be with '/'). + * second one being the last segment, (guaranteed to be without a '/'). */ public static String[] getParentPath(String path) { - int parentIndex = path.lastIndexOf('/'); - // TODO make it more robust - return new String[] { parentIndex != 0 ? path.substring(0, parentIndex) : "/", + if (path == null) + throw new IllegalArgumentException("Path cannot be null"); + if (path.length() == 0) + throw new IllegalArgumentException("Path cannot be empty"); + checkDoubleSlash(path); + int parentIndex = path.lastIndexOf(SLASH); + if (parentIndex == path.length() - 1) {// trailing '/' + path = path.substring(0, path.length() - 1); + parentIndex = path.lastIndexOf(SLASH); + } + + if (parentIndex == -1) // no '/' + return new String[] { "", path }; + + return new String[] { parentIndex != 0 ? path.substring(0, parentIndex) : "" + SLASH, path.substring(parentIndex + 1) }; } + public static List toPathSegments(String path) { + List res = new ArrayList<>(); + if ("".equals(path) || ROOT_SLASH.equals(path)) + return res; + collectPathSegments(path, res); + return res; + } + + private static void collectPathSegments(String path, List segments) { + String[] parent = getParentPath(path); + if ("".equals(parent[1])) // root + return; + segments.add(0, parent[1]); + if ("".equals(parent[0])) // end + return; + collectPathSegments(parent[0], segments); + } + + public static void checkDoubleSlash(String path) { + if (path.contains(SLASH + "" + SLASH)) + throw new IllegalArgumentException("Path " + path + " contains //"); + } + /** Singleton. */ private ContentUtils() { diff --git a/org.argeo.cms/src/org/argeo/cms/acr/TypesManager.java b/org.argeo.cms/src/org/argeo/cms/acr/TypesManager.java index 62335d361..731c6d562 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/TypesManager.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/TypesManager.java @@ -42,6 +42,7 @@ import org.apache.xerces.xs.XSSimpleTypeDefinition; import org.apache.xerces.xs.XSTerm; import org.apache.xerces.xs.XSTypeDefinition; import org.argeo.api.acr.CrAttributeType; +import org.argeo.api.acr.NamespaceUtils; import org.argeo.api.cms.CmsLog; import org.xml.sax.ErrorHandler; import org.xml.sax.SAXException; @@ -72,13 +73,7 @@ class TypesManager { schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); // types - types = new TreeMap<>((qn1, qn2) -> { - if (Objects.equals(qn1.getNamespaceURI(), qn2.getNamespaceURI())) {// same namespace - return qn1.getLocalPart().compareTo(qn2.getLocalPart()); - } else { - return qn1.getNamespaceURI().compareTo(qn2.getNamespaceURI()); - } - }); + types = new TreeMap<>(NamespaceUtils.QNAME_COMPARATOR); } diff --git a/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java b/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java new file mode 100644 index 000000000..60ef67551 --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java @@ -0,0 +1,145 @@ +package org.argeo.cms.acr.directory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.xml.namespace.QName; + +import org.argeo.api.acr.Content; +import org.argeo.api.acr.ContentName; +import org.argeo.api.acr.ContentNotFoundException; +import org.argeo.api.acr.CrName; +import org.argeo.api.acr.spi.ContentProvider; +import org.argeo.api.acr.spi.ProvidedContent; +import org.argeo.api.acr.spi.ProvidedSession; +import org.argeo.cms.CmsUserManager; +import org.argeo.cms.acr.AbstractContent; +import org.argeo.cms.acr.ContentUtils; +import org.argeo.osgi.useradmin.HierarchyUnit; +import org.argeo.osgi.useradmin.LdapNameUtils; +import org.argeo.osgi.useradmin.UserDirectory; +import org.osgi.service.useradmin.User; + +public class DirectoryContentProvider implements ContentProvider { + private String mountPath; + private String mountName; + + private CmsUserManager userManager; + + public DirectoryContentProvider(String mountPath, CmsUserManager userManager) { + this.mountPath = mountPath; + List mountSegments = ContentUtils.toPathSegments(mountPath); + this.mountName = mountSegments.get(mountSegments.size() - 1); + this.userManager = userManager; + } + + @Override + public ProvidedContent get(ProvidedSession session, String relativePath) { + List segments = ContentUtils.toPathSegments(relativePath); + if (segments.size() == 0) + return new UserManagerContent(session); + String userDirectoryDn = segments.get(0); + UserDirectory userDirectory = null; + userDirectories: for (UserDirectory ud : userManager.getUserDirectories()) { + if (userDirectoryDn.equals(ud.getBasePath())) { + userDirectory = ud; + break userDirectories; + } + } + if (userDirectory == null) + throw new ContentNotFoundException("Cannot find user directory " + userDirectoryDn); + if (segments.size() == 1) { + return new HierarchyUnitContent(session, this, userDirectory); + } else { + LdapName dn; + try { + dn = LdapNameUtils.toLdapName(userDirectoryDn); + for (int i = 1; i < segments.size(); i++) { + dn.add(segments.get(i)); + } + } catch (InvalidNameException e) { + throw new IllegalStateException("Cannot interpret " + segments + " as DN", e); + } + User user = userManager.getUser(dn.toString()); + if (user != null) { + HierarchyUnit parent = userDirectory.getHierarchyUnit(user); + return new RoleContent(session, this, new HierarchyUnitContent(session, this, parent), user); + } + HierarchyUnit hierarchyUnit = userDirectory.getHierarchyUnit(dn.toString()); + if (hierarchyUnit == null) + throw new ContentNotFoundException("Cannot find " + dn); + return new HierarchyUnitContent(session, this, hierarchyUnit); + } + } + + @Override + public boolean exists(ProvidedSession session, String relativePath) { + // TODO Auto-generated method stub + return false; + } + + @Override + public String getMountPath() { + return mountPath; + } + + @Override + public String getNamespaceURI(String prefix) { + if (CrName.LDAP_DEFAULT_PREFIX.equals(prefix)) + return CrName.LDAP_NAMESPACE_URI; + throw new IllegalArgumentException("Only prefix " + CrName.LDAP_DEFAULT_PREFIX + " is supported"); + } + + @Override + public Iterator getPrefixes(String namespaceURI) { + if (CrName.LDAP_NAMESPACE_URI.equals(namespaceURI)) + return Collections.singletonList(CrName.LDAP_DEFAULT_PREFIX).iterator(); + throw new IllegalArgumentException("Only namespace URI " + CrName.LDAP_NAMESPACE_URI + " is supported"); + } + + public void setUserManager(CmsUserManager userManager) { + this.userManager = userManager; + } + + UserManagerContent getRootContent(ProvidedSession session) { + return new UserManagerContent(session); + } + + class UserManagerContent extends AbstractContent { + + public UserManagerContent(ProvidedSession session) { + super(session); + } + + @Override + public ContentProvider getProvider() { + return DirectoryContentProvider.this; + } + + @Override + public QName getName() { + return new ContentName(mountName); + } + + @Override + public Content getParent() { + return null; + } + + @Override + public Iterator iterator() { + List res = new ArrayList<>(); + for (UserDirectory userDirectory : userManager.getUserDirectories()) { + HierarchyUnitContent content = new HierarchyUnitContent(getSession(), DirectoryContentProvider.this, + userDirectory); + res.add(content); + } + return res.iterator(); + } + + } +} diff --git a/org.argeo.cms/src/org/argeo/cms/acr/directory/HierarchyUnitContent.java b/org.argeo.cms/src/org/argeo/cms/acr/directory/HierarchyUnitContent.java new file mode 100644 index 000000000..9184c63af --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/acr/directory/HierarchyUnitContent.java @@ -0,0 +1,78 @@ +package org.argeo.cms.acr.directory; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +import javax.xml.namespace.QName; + +import org.argeo.api.acr.Content; +import org.argeo.api.acr.ContentName; +import org.argeo.api.acr.CrName; +import org.argeo.api.acr.spi.ContentProvider; +import org.argeo.api.acr.spi.ProvidedSession; +import org.argeo.cms.acr.AbstractContent; +import org.argeo.osgi.useradmin.HierarchyUnit; +import org.osgi.service.useradmin.Role; + +public class HierarchyUnitContent extends AbstractContent { + private HierarchyUnit hierarchyUnit; + + private DirectoryContentProvider provider; + + public HierarchyUnitContent(ProvidedSession session, DirectoryContentProvider provider, + HierarchyUnit hierarchyUnit) { + super(session); + Objects.requireNonNull(hierarchyUnit); + this.provider = provider; + this.hierarchyUnit = hierarchyUnit; + } + + @Override + public ContentProvider getProvider() { + return provider; + } + + @Override + public QName getName() { + if (hierarchyUnit.getParent() == null) {// base DN + String baseDn = hierarchyUnit.getBasePath(); + return new ContentName(baseDn); + } + String name = hierarchyUnit.getHierarchyUnitName(); + return new ContentName(name); + } + + @Override + public Content getParent() { + HierarchyUnit parentHu = hierarchyUnit.getParent(); + if (parentHu == null) { + return provider.getRootContent(getSession()); + } + return new HierarchyUnitContent(getSession(), provider, parentHu); + } + + @Override + public Iterator iterator() { + List lst = new ArrayList<>(); + for (int i = 0; i < hierarchyUnit.getHierarchyChildCount(); i++) + lst.add(new HierarchyUnitContent(getSession(), provider, hierarchyUnit.getHierarchyChild(i))); + + for (Role role : hierarchyUnit.getRoles(null, false)) + lst.add(new RoleContent(getSession(), provider, this, role)); + return lst.iterator(); + } + + /* + * TYPING + */ + + @Override + public List getTypes() { + List res = new ArrayList<>(); + res.add(CrName.COLLECTION.get()); + return res; + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/acr/directory/RoleContent.java b/org.argeo.cms/src/org/argeo/cms/acr/directory/RoleContent.java new file mode 100644 index 000000000..8043205ec --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/acr/directory/RoleContent.java @@ -0,0 +1,125 @@ +package org.argeo.cms.acr.directory; + +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; + +import javax.naming.ldap.LdapName; +import javax.swing.GroupLayout.Group; +import javax.xml.namespace.QName; + +import org.argeo.api.acr.Content; +import org.argeo.api.acr.ContentName; +import org.argeo.api.acr.CrName; +import org.argeo.api.acr.NamespaceUtils; +import org.argeo.api.acr.spi.ContentProvider; +import org.argeo.api.acr.spi.ProvidedSession; +import org.argeo.cms.acr.AbstractContent; +import org.argeo.osgi.useradmin.LdapNameUtils; +import org.argeo.util.naming.LdapAttrs; +import org.argeo.util.naming.LdapObjs; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.User; + +public class RoleContent extends AbstractContent { + + private DirectoryContentProvider provider; + private HierarchyUnitContent parent; + private Role role; + + public RoleContent(ProvidedSession session, DirectoryContentProvider provider, HierarchyUnitContent parent, + Role role) { + super(session); + this.provider = provider; + this.parent = parent; + this.role = role; + } + + @Override + public ContentProvider getProvider() { + return provider; + } + + @Override + public QName getName() { + LdapName dn = LdapNameUtils.toLdapName(role.getName()); + String name = LdapNameUtils.getLastRdnAsString(dn); + return new ContentName(name); + } + + @Override + public Content getParent() { + return parent; + } + + @SuppressWarnings("unchecked") + @Override + public Optional get(QName key, Class clss) { + String attrName = key.getLocalPart(); + Object value = role.getProperties().get(attrName); + if (value == null) + return Optional.empty(); + // TODO deal with type and multiple + return Optional.of((A) value); + } + + @Override + protected Iterable keys() { + Set keys = new TreeSet<>(NamespaceUtils.QNAME_COMPARATOR); + keys: for (Enumeration it = role.getProperties().keys(); it.hasMoreElements();) { + String key = it.nextElement(); + if (key.equalsIgnoreCase(LdapAttrs.objectClass.name())) + continue keys; + ContentName name = new ContentName(CrName.LDAP_NAMESPACE_URI, key, provider); + keys.add(name); + } + return keys; + } + + @Override + public List getTypes() { + List contentClasses = new ArrayList<>(); + keys: for (Enumeration it = role.getProperties().keys(); it.hasMoreElements();) { + String key = it.nextElement(); + if (key.equalsIgnoreCase(LdapAttrs.objectClass.name())) { + String[] objectClasses = role.getProperties().get(key).toString().split("\\n"); + objectClasses: for (String objectClass : objectClasses) { + if (LdapObjs.top.name().equalsIgnoreCase(objectClass)) + continue objectClasses; + contentClasses.add(new ContentName(CrName.LDAP_NAMESPACE_URI, objectClass, provider)); + } + break keys; + } + } + return contentClasses; + } + + @Override + public Object put(QName key, Object value) { + Object previous = get(key); + // TODO deal with typing + role.getProperties().put(key.getLocalPart(), value); + return previous; + } + + @Override + protected void removeAttr(QName key) { + role.getProperties().remove(key.getLocalPart()); + } + + @SuppressWarnings("unchecked") + @Override + public A adapt(Class clss) { + if (clss.equals(Group.class)) + return (A) role; + else if (clss.equals(User.class)) + return (A) role; + else if (clss.equals(Role.class)) + return (A) role; + return super.adapt(clss); + } + +} diff --git a/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java b/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java index 5c9c1096e..d0df8845f 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/fs/FsContent.java @@ -11,10 +11,12 @@ import java.nio.file.Path; import java.nio.file.attribute.FileTime; import java.nio.file.attribute.UserDefinedFileAttributeView; import java.time.Instant; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -52,14 +54,13 @@ public class FsContent extends AbstractContent implements ProvidedContent { POSIX_KEYS.put(CrName.PERMISSIONS.get(), "posix:permissions"); } - private final ProvidedSession session; private final FsContentProvider provider; private final Path path; private final boolean isRoot; private final QName name; protected FsContent(ProvidedSession session, FsContentProvider contentProvider, Path path) { - this.session = session; + super(session); this.provider = contentProvider; this.path = path; this.isRoot = contentProvider.isMountRoot(path); @@ -159,7 +160,7 @@ public class FsContent extends AbstractContent implements ProvidedContent { try { for (String name : udfav.list()) { QName providerName = NamespaceUtils.parsePrefixedName(provider, name); - QName sessionName = new ContentName(providerName, session); + QName sessionName = new ContentName(providerName, getSession()); result.add(sessionName); } } catch (IOException e) { @@ -211,9 +212,9 @@ public class FsContent extends AbstractContent implements ProvidedContent { Optional isMount = fsContent.get(CrName.MOUNT.get(), String.class); if (isMount.orElse("false").equals("true")) { QName[] classes = null; - ContentProvider contentProvider = session.getRepository().getMountContentProvider(fsContent, - false, classes); - Content mountedContent = contentProvider.get(session, ""); + ContentProvider contentProvider = getSession().getRepository() + .getMountContentProvider(fsContent, false, classes); + Content mountedContent = contentProvider.get(getSession(), ""); return mountedContent; } else { return (Content) fsContent; @@ -245,9 +246,10 @@ public class FsContent extends AbstractContent implements ProvidedContent { throw new ContentResourceException("Cannot create new content", e); } - if (session.getRepository().shouldMount(classes)) { - ContentProvider contentProvider = session.getRepository().getMountContentProvider(fsContent, true, classes); - Content mountedContent = contentProvider.get(session, ""); + if (getSession().getRepository().shouldMount(classes)) { + ContentProvider contentProvider = getSession().getRepository().getMountContentProvider(fsContent, true, + classes); + Content mountedContent = contentProvider.get(getSession(), ""); fsContent.put(CrName.MOUNT.get(), "true"); return mountedContent; @@ -268,7 +270,7 @@ public class FsContent extends AbstractContent implements ProvidedContent { if (mountPath == null || mountPath.equals("/")) return null; String[] parent = ContentUtils.getParentPath(mountPath); - return session.get(parent[0]); + return getSession().get(parent[0]); } return new FsContent(this, path.getParent()); } @@ -299,13 +301,22 @@ public class FsContent extends AbstractContent implements ProvidedContent { } /* - * ACCESSORS + * TYPING */ + @Override - public ProvidedSession getSession() { - return session; + public List getTypes() { + List res = new ArrayList<>(); + if (Files.isDirectory(path)) + res.add(CrName.COLLECTION.get()); + // TODO add other types + return res; } + /* + * ACCESSORS + */ + @Override public FsContentProvider getProvider() { return provider; diff --git a/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContent.java b/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContent.java index ac863403d..15dfe291b 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/xml/DomContent.java @@ -1,8 +1,11 @@ package org.argeo.cms.acr.xml; import java.nio.CharBuffer; +import java.nio.file.Files; +import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; +import java.util.List; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; @@ -13,6 +16,7 @@ import javax.xml.namespace.QName; import org.argeo.api.acr.Content; import org.argeo.api.acr.ContentName; +import org.argeo.api.acr.CrName; import org.argeo.api.acr.spi.ProvidedContent; import org.argeo.api.acr.spi.ProvidedSession; import org.argeo.cms.acr.AbstractContent; @@ -28,7 +32,6 @@ import org.w3c.dom.Text; /** Content persisted as a DOM element. */ public class DomContent extends AbstractContent implements ProvidedContent { - private final ProvidedSession session; private final DomContentProvider provider; private final Element element; @@ -36,7 +39,7 @@ public class DomContent extends AbstractContent implements ProvidedContent { private Boolean hasText = null; public DomContent(ProvidedSession session, DomContentProvider contentProvider, Element element) { - this.session = session; + super(session); this.provider = contentProvider; this.element = element; } @@ -50,7 +53,7 @@ public class DomContent extends AbstractContent implements ProvidedContent { if (element.getParentNode() == null) {// root String mountPath = provider.getMountPath(); if (mountPath != null) { - Content mountPoint = session.getMountPoint(mountPath); + Content mountPoint = getSession().getMountPoint(mountPath); return mountPoint.getName(); } } @@ -198,7 +201,7 @@ public class DomContent extends AbstractContent implements ProvidedContent { @Override public Iterator iterator() { NodeList nodeList = element.getChildNodes(); - return new ElementIterator(this, session, provider, nodeList); + return new ElementIterator(this, getSession(), provider, nodeList); } @Override @@ -209,7 +212,7 @@ public class DomContent extends AbstractContent implements ProvidedContent { if (mountPath == null) return null; String[] parent = ContentUtils.getParentPath(mountPath); - return session.get(parent[0]); + return getSession().get(parent[0]); } if (parentNode instanceof Document) return null; @@ -263,7 +266,7 @@ public class DomContent extends AbstractContent implements ProvidedContent { if (String.class.isAssignableFrom(clss)) { CompletableFuture res = new CompletableFuture<>(); res.thenAccept((s) -> { - session.notifyModification(this); + getSession().notifyModification(this); element.setTextContent(s); }); return (CompletableFuture) res; @@ -271,6 +274,16 @@ public class DomContent extends AbstractContent implements ProvidedContent { return super.write(clss); } + /* + * TYPING + */ + @Override + public List getTypes() { + List res = new ArrayList<>(); + res.add(getName()); + return res; + } + /* * MOUNT MANAGEMENT */ @@ -282,10 +295,7 @@ public class DomContent extends AbstractContent implements ProvidedContent { return new DomContent(this, childElement); } - public ProvidedSession getSession() { - return session; - } - + @Override public DomContentProvider getProvider() { return provider; } 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 84562ebd1..135bf50c8 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 @@ -16,7 +16,9 @@ import java.util.Hashtable; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.NavigableMap; import java.util.Set; +import java.util.TreeMap; import java.util.TreeSet; import java.util.UUID; @@ -438,6 +440,20 @@ public class CmsUserManagerImpl implements CmsUserManager { } } + @Override + public UserDirectory getUserDirectory(User user) { + String name = user.getName(); + NavigableMap possible = new TreeMap<>(); + for (UserDirectory userDirectory : userDirectories.keySet()) { + if (name.endsWith(userDirectory.getBasePath())) { + possible.put(userDirectory.getBasePath(), userDirectory); + } + } + if (possible.size() == 0) + throw new IllegalStateException("No user directory found for user " + name); + return possible.lastEntry().getValue(); + } + // public User createUserFromPerson(Node person) { // String email = JcrUtils.get(person, LdapAttrs.mail.property()); // String dn = buildDefaultDN(email, Role.USER); diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/DeployedContentRepository.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/DeployedContentRepository.java index cff9bc514..4a4a4d986 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/DeployedContentRepository.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/DeployedContentRepository.java @@ -6,13 +6,16 @@ import java.nio.file.Path; import java.util.Map; import org.argeo.api.acr.spi.ContentProvider; +import org.argeo.cms.CmsUserManager; import org.argeo.cms.acr.CmsContentRepository; +import org.argeo.cms.acr.directory.DirectoryContentProvider; import org.argeo.cms.acr.fs.FsContentProvider; -import org.argeo.util.OS; public class DeployedContentRepository extends CmsContentRepository { private final static String ROOT_XML = "cr:root.xml"; + private CmsUserManager userManager; + @Override public void start() { try { @@ -24,10 +27,16 @@ public class DeployedContentRepository extends CmsContentRepository { // FsContentProvider srvContentProvider = new FsContentProvider("/" + CmsConstants.SRV_WORKSPACE, srvPath, false); // addProvider(srvContentProvider); - Path runDirPath = KernelUtils.getOsgiInstancePath(CmsContentRepository.RUN_BASE); + // run dir + Path runDirPath = KernelUtils.getOsgiInstancePath(CmsContentRepository.RUN_BASE); Files.createDirectories(runDirPath); FsContentProvider runContentProvider = new FsContentProvider(CmsContentRepository.RUN_BASE, runDirPath); addProvider(runContentProvider); + + // users + DirectoryContentProvider directoryContentProvider = new DirectoryContentProvider( + CmsContentRepository.DIRECTORY_BASE, userManager); + addProvider(directoryContentProvider); } catch (IOException e) { throw new IllegalStateException("Cannot start content repository", e); } @@ -46,4 +55,8 @@ public class DeployedContentRepository extends CmsContentRepository { public void removeContentProvider(ContentProvider provider, Map properties) { } + public void setUserManager(CmsUserManager userManager) { + this.userManager = userManager; + } + } diff --git a/org.argeo.cms/src/org/argeo/cms/runtime/StaticCms.java b/org.argeo.cms/src/org/argeo/cms/runtime/StaticCms.java index e4087e139..a44c5d4fd 100644 --- a/org.argeo.cms/src/org/argeo/cms/runtime/StaticCms.java +++ b/org.argeo.cms/src/org/argeo/cms/runtime/StaticCms.java @@ -3,6 +3,7 @@ package org.argeo.cms.runtime; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Dictionary; +import java.util.HashMap; import java.util.concurrent.CompletableFuture; import org.argeo.api.acr.ContentRepository; @@ -11,7 +12,9 @@ import org.argeo.api.cms.CmsContext; import org.argeo.api.cms.CmsDeployment; import org.argeo.api.cms.CmsState; import org.argeo.api.uuid.UuidFactory; +import org.argeo.cms.CmsUserManager; import org.argeo.cms.acr.CmsUuidFactory; +import org.argeo.cms.internal.auth.CmsUserManagerImpl; import org.argeo.cms.internal.osgi.DeployConfig; import org.argeo.cms.internal.runtime.CmsContextImpl; import org.argeo.cms.internal.runtime.CmsDeploymentImpl; @@ -21,6 +24,7 @@ import org.argeo.cms.internal.runtime.DeployedContentRepository; import org.argeo.osgi.transaction.SimpleTransactionManager; import org.argeo.osgi.transaction.WorkControl; import org.argeo.osgi.transaction.WorkTransaction; +import org.argeo.osgi.useradmin.UserDirectory; import org.argeo.util.register.Component; import org.argeo.util.register.SimpleRegister; import org.osgi.service.useradmin.UserAdmin; @@ -77,7 +81,6 @@ public class StaticCms { // User Admin CmsUserAdmin userAdmin = new CmsUserAdmin(); - Component userAdminC = new Component.Builder<>(userAdmin) // .addType(UserAdmin.class) // .addDependency(transactionManagerC.getType(WorkControl.class), userAdmin::setTransactionManager, null) // @@ -88,6 +91,19 @@ public class StaticCms { }, null) // .build(register); + // User manager + CmsUserManagerImpl userManager = new CmsUserManagerImpl(); + for (UserDirectory userDirectory : userAdmin.getUserDirectories()) { + // FIXME deal with properties + userManager.addUserDirectory(userDirectory, new HashMap<>()); + } + Component userManagerC = new Component.Builder<>(userManager) // + .addType(CmsUserManager.class) // + .addDependency(userAdminC.getType(UserAdmin.class), userManager::setUserAdmin, null) // + .addDependency(transactionManagerC.getType(WorkTransaction.class), userManager::setUserTransaction, + null) // + .build(register); + // Content Repository DeployedContentRepository contentRepository = new DeployedContentRepository(); Component contentRepositoryC = new Component.Builder<>(contentRepository) // @@ -97,6 +113,7 @@ public class StaticCms { .addDeactivation(contentRepository::stop) // .addDependency(cmsStateC.getType(CmsState.class), contentRepository::setCmsState, null) // .addDependency(uuidFactoryC.getType(UuidFactory.class), contentRepository::setUuidFactory, null) // + .addDependency(userManagerC.getType(CmsUserManager.class), contentRepository::setUserManager, null) // .build(register); // CMS Context 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 716ddb5ed..889f9cfa7 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java @@ -444,6 +444,11 @@ abstract class AbstractUserDirectory implements UserAdmin, UserDirectory { throw new IllegalArgumentException("No child hierarchy unit available"); } + @Override + public HierarchyUnit getParent() { + return null; + } + @Override public int getHierarchyUnitType() { return 0; @@ -451,11 +456,21 @@ abstract class AbstractUserDirectory implements UserAdmin, UserDirectory { @Override public String getHierarchyUnitName() { - String name = baseDn.getRdn(baseDn.size() - 1).getValue().toString(); + String name = LdapNameUtils.getLastRdnAsString(baseDn); // TODO check ou, o, etc. return name; } + @Override + public HierarchyUnit getHierarchyUnit(String path) { + return null; + } + + @Override + public HierarchyUnit getHierarchyUnit(Role role) { + return null; + } + @Override public List getRoles(String filter, boolean deep) { try { diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java b/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java index 5613c2848..ad6a83fb5 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java @@ -9,6 +9,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.TreeSet; import javax.naming.InvalidNameException; import javax.naming.ldap.LdapName; @@ -270,4 +271,9 @@ public class AggregatingUserAdmin implements UserAdmin { protected void preDestroy(UserDirectory userDirectory) { } + public Set getUserDirectories() { + TreeSet res = new TreeSet<>((o1, o2) -> o1.getBasePath().compareTo(o2.getBasePath())); + res.addAll(businessRoles.values()); + return res; + } } diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/HierarchyUnit.java b/org.argeo.util/src/org/argeo/osgi/useradmin/HierarchyUnit.java index c01ea15f4..249434631 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/HierarchyUnit.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/HierarchyUnit.java @@ -1,6 +1,7 @@ package org.argeo.osgi.useradmin; import java.util.List; +import java.util.Map; import org.osgi.service.useradmin.Role; @@ -14,6 +15,8 @@ public interface HierarchyUnit { int getHierarchyChildCount(); + HierarchyUnit getParent(); + HierarchyUnit getHierarchyChild(int i); int getHierarchyUnitType(); @@ -21,4 +24,6 @@ public interface HierarchyUnit { String getBasePath(); List getRoles(String filter, boolean deep); + +// Map getHierarchyProperties(); } diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdapNameUtils.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdapNameUtils.java new file mode 100644 index 000000000..b98c8bff7 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdapNameUtils.java @@ -0,0 +1,58 @@ +package org.argeo.osgi.useradmin; + +import java.util.StringJoiner; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; + +/** Utilities to simplify using {@link LdapName}. */ +public class LdapNameUtils { + + public static String toRevertPath(String dn, String prefix) { + if (!dn.endsWith(prefix)) + throw new IllegalArgumentException("Prefix " + prefix + " not consistent with " + dn); + String relativeName = dn.substring(0, dn.length() - prefix.length() - 1); + LdapName name = toLdapName(relativeName); + StringJoiner path = new StringJoiner("/"); + for (int i = 0; i < name.size(); i++) { + path.add(name.get(i)); + } + return path.toString(); + } + + public static LdapName getParent(LdapName dn) { + try { + LdapName parent = (LdapName) dn.clone(); + parent.remove(parent.size() - 1); + return parent; + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Cannot get parent of " + dn, e); + } + } + + public static LdapName toLdapName(String distinguishedName) { + try { + return new LdapName(distinguishedName); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Cannot parse " + distinguishedName + " as LDAP name", e); + } + } + + public static Rdn getLastRdn(LdapName dn) { + return dn.getRdn(dn.size() - 1); + } + + public static String getLastRdnAsString(LdapName dn) { + return getLastRdn(dn).toString(); + } + + public static String getLastRdnValue(LdapName dn) { + return getLastRdn(dn).getValue().toString(); + } + + /** singleton */ + private LdapNameUtils() { + + } +} 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 623ca2a92..af75bf03e 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdapUserAdmin.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdapUserAdmin.java @@ -187,4 +187,11 @@ public class LdapUserAdmin extends AbstractUserDirectory { // prepare not impacting } +// @Override +// public HierarchyUnit getHierarchyUnit(String path) { +// LdapName dn = LdapNameUtils.toLdapName(path); +// Attributes attrs = ldapConnection.getAttributes(dn); +// +// } + } diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifHierarchyUnit.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifHierarchyUnit.java index 5cf52b9a2..593416259 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifHierarchyUnit.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifHierarchyUnit.java @@ -2,6 +2,7 @@ package org.argeo.osgi.useradmin; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Objects; import javax.naming.directory.Attributes; @@ -36,6 +37,11 @@ class LdifHierarchyUnit implements HierarchyUnit { return children.size(); } + @Override + public HierarchyUnit getParent() { + return parent; + } + @Override public HierarchyUnit getHierarchyChild(int i) { return children.get(i); @@ -48,7 +54,7 @@ class LdifHierarchyUnit implements HierarchyUnit { @Override public String getHierarchyUnitName() { - String name = dn.getRdn(dn.size() - 1).getValue().toString(); + String name = LdapNameUtils.getLastRdnAsString(dn); // TODO check ou, o, etc. return name; } diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java index 135645a12..c03465b70 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java @@ -14,6 +14,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; +import java.util.StringJoiner; import javax.naming.NamingEnumeration; import javax.naming.NamingException; @@ -156,7 +157,7 @@ class LdifUser implements DirectoryUser { byte[] sha1hash(char[] password) { byte[] hashedPassword = ("{SHA}" + Base64.getEncoder().encodeToString(DigestUtils.sha1(DigestUtils.charsToBytes(password)))) - .getBytes(StandardCharsets.UTF_8); + .getBytes(StandardCharsets.UTF_8); return hashedPassword; } @@ -307,24 +308,25 @@ class LdifUser implements DirectoryUser { } if (attr.size() == 1) return value; - if (!attr.getID().equals(LdapAttrs.objectClass.name())) - return value; +// if (!attr.getID().equals(LdapAttrs.objectClass.name())) +// return value; // special case for object class NamingEnumeration en = attr.getAll(); - Set objectClasses = new HashSet(); + StringJoiner values = new StringJoiner("\n"); + // Set values = new HashSet(); while (en.hasMore()) { - String objectClass = en.next().toString(); - objectClasses.add(objectClass); + String v = en.next().toString(); + values.add(v); } - - if (objectClasses.contains(userAdmin.getUserObjectClass())) - return userAdmin.getUserObjectClass(); - else if (objectClasses.contains(userAdmin.getGroupObjectClass())) - return userAdmin.getGroupObjectClass(); - else - return value; + return values.toString(); +// if (objectClasses.contains(userAdmin.getUserObjectClass())) +// return userAdmin.getUserObjectClass(); +// else if (objectClasses.contains(userAdmin.getGroupObjectClass())) +// return userAdmin.getGroupObjectClass(); +// else +// return value; } catch (NamingException e) { - throw new UserDirectoryException("Cannot get value for attribute " + key, e); + throw new IllegalStateException("Cannot get value for attribute " + key, e); } } 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 f9163d7e2..6d9305a17 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUserAdmin.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUserAdmin.java @@ -308,6 +308,9 @@ public class LdifUserAdmin extends AbstractUserDirectory { init(); } + /* + * HIERARCHY + */ @Override public int getHierarchyChildCount() { return rootHierarchyUnits.size(); @@ -318,8 +321,21 @@ public class LdifUserAdmin extends AbstractUserDirectory { return rootHierarchyUnits.get(i); } - /* - * HIERARCHY - */ + @Override + public HierarchyUnit getHierarchyUnit(String path) { + LdapName dn = LdapNameUtils.toLdapName(path); + return hierarchy.get(dn); + } + + @Override + public HierarchyUnit getHierarchyUnit(Role role) { + LdapName dn = LdapNameUtils.toLdapName(role.getName()); + // 2 levels + LdapName huDn = LdapNameUtils.getParent(LdapNameUtils.getParent(dn)); + HierarchyUnit hierarchyUnit = hierarchy.get(huDn); + if (hierarchyUnit == null) + throw new IllegalStateException("No hierarchy unit found for " + role); + return hierarchyUnit; + } } 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 5a69f1a16..781b7855a 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectory.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectory.java @@ -3,6 +3,7 @@ package org.argeo.osgi.useradmin; import java.util.Optional; import org.argeo.osgi.transaction.WorkControl; +import org.osgi.service.useradmin.Role; /** Information about a user directory. */ public interface UserDirectory extends HierarchyUnit { @@ -31,7 +32,11 @@ public interface UserDirectory extends HierarchyUnit { String getGroupBase(); Optional getRealm(); - + + HierarchyUnit getHierarchyUnit(String path); + + HierarchyUnit getHierarchyUnit(Role role); + @Deprecated void setTransactionControl(WorkControl transactionControl); } -- 2.30.2