From 0ce8ecfe974cec9f524c16884209cd08544d890d Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Thu, 23 Jun 2022 09:02:49 +0200 Subject: [PATCH] Simplify LDAP directory. --- .../argeo/cms/swt/widgets/SwtTabularPart.java | 23 +- .../src/org/argeo/cms/ux/widgets/Column.java | 8 +- .../directory/AbstractDirectoryContent.java | 96 ++++ .../cms/acr/directory/DirectoryContent.java | 13 +- .../directory/DirectoryContentProvider.java | 3 + .../acr/directory/HierarchyUnitContent.java | 29 +- .../argeo/cms/acr/directory/RoleContent.java | 79 +--- .../org/argeo/cms/auth/UserAdminUtils.java | 19 +- .../osgi/useradmin/DirectoryUserAdmin.java | 17 +- .../org/argeo/osgi/useradmin/LdifGroup.java | 32 +- .../org/argeo/osgi/useradmin/LdifUser.java | 339 +------------- .../argeo/osgi/useradmin/OsUserDirectory.java | 4 +- .../org/argeo/util/directory/Directory.java | 6 + .../argeo/util/directory/FunctionalGroup.java | 5 - .../argeo/util/directory/HierarchyUnit.java | 4 +- .../argeo/util/directory/Organization.java | 5 - .../src/org/argeo/util/directory/Person.java | 5 - .../util/directory/SystemPermissions.java | 5 - .../directory/ldap/AbstractLdapDirectory.java | 63 ++- .../directory/ldap/AbstractLdapEntry.java | 114 ----- .../util/directory/ldap/DefaultLdapEntry.java | 431 ++++++++++++++++++ .../argeo/util/directory/ldap/LdapDao.java | 44 +- .../util/directory/ldap/LdapDirectoryDao.java | 8 +- .../argeo/util/directory/ldap/LdapEntry.java | 6 + .../directory/ldap/LdapHierarchyUnit.java | 2 +- .../argeo/util/directory/ldap/LdifDao.java | 8 +- 26 files changed, 697 insertions(+), 671 deletions(-) create mode 100644 org.argeo.cms/src/org/argeo/cms/acr/directory/AbstractDirectoryContent.java delete mode 100644 org.argeo.util/src/org/argeo/util/directory/FunctionalGroup.java delete mode 100644 org.argeo.util/src/org/argeo/util/directory/Organization.java delete mode 100644 org.argeo.util/src/org/argeo/util/directory/Person.java delete mode 100644 org.argeo.util/src/org/argeo/util/directory/SystemPermissions.java delete mode 100644 org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapEntry.java create mode 100644 org.argeo.util/src/org/argeo/util/directory/ldap/DefaultLdapEntry.java 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 index 9872551be..af4c68aac 100644 --- 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 @@ -2,12 +2,15 @@ package org.argeo.cms.swt.widgets; import java.util.function.Consumer; +import org.argeo.api.cms.ux.CmsIcon; +import org.argeo.cms.swt.CmsSwtTheme; import org.argeo.cms.swt.CmsSwtUtils; import org.argeo.cms.ux.widgets.Column; 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.graphics.Image; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; @@ -22,7 +25,10 @@ public class SwtTabularPart implements TabularPart { private Consumer onSelected; private Consumer onAction; + private CmsSwtTheme theme; + public SwtTabularPart(Composite parent, int style) { + theme = CmsSwtUtils.getCmsTheme(parent); area = new Composite(parent, style); area.setLayout(CmsSwtUtils.noSpaceGridLayout()); table = new Table(area, SWT.VIRTUAL | SWT.BORDER); @@ -46,18 +52,25 @@ public class SwtTabularPart implements TabularPart { @Override public void widgetSelected(SelectionEvent e) { if (onSelected != null) - onSelected.accept(e.item.getData()); + onSelected.accept(getDataFromEvent(e)); } @Override public void widgetDefaultSelected(SelectionEvent e) { if (onAction != null) - onAction.accept(e.item.getData()); + onAction.accept(getDataFromEvent(e)); } }); } + protected Object getDataFromEvent(SelectionEvent e) { + Object data = e.item.getData(); + if (data == null) + data = getData(getTable().indexOf((TableItem) e.item)); + return data; + } + @Override public void setInput(Object data) { area.setData(data); @@ -74,8 +87,14 @@ public class SwtTabularPart implements TabularPart { for (int i = 0; i < item.getParent().getColumnCount(); i++) { Column column = (Column) item.getParent().getColumn(i).getData(); Object data = getData(row); + item.setData(data); String text = data != null ? column.getText(data) : ""; item.setText(i, text); + CmsIcon icon = column.getIcon(data); + if (icon != null) { + Image image = theme.getSmallIcon(icon); + item.setImage(i, image); + } } } diff --git a/org.argeo.cms.ux/src/org/argeo/cms/ux/widgets/Column.java b/org.argeo.cms.ux/src/org/argeo/cms/ux/widgets/Column.java index 9bfa9620a..973fddb5a 100644 --- a/org.argeo.cms.ux/src/org/argeo/cms/ux/widgets/Column.java +++ b/org.argeo.cms.ux/src/org/argeo/cms/ux/widgets/Column.java @@ -1,10 +1,16 @@ package org.argeo.cms.ux.widgets; +import org.argeo.api.cms.ux.CmsIcon; + public interface Column { String getText(T model); default int getWidth() { return 200; } - + + default CmsIcon getIcon(T model) { + return null; + } + } diff --git a/org.argeo.cms/src/org/argeo/cms/acr/directory/AbstractDirectoryContent.java b/org.argeo.cms/src/org/argeo/cms/acr/directory/AbstractDirectoryContent.java new file mode 100644 index 000000000..62148b9af --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/acr/directory/AbstractDirectoryContent.java @@ -0,0 +1,96 @@ +package org.argeo.cms.acr.directory; + +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.TreeSet; + +import javax.xml.namespace.QName; + +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.util.naming.LdapAttrs; +import org.argeo.util.naming.LdapObjs; + +abstract class AbstractDirectoryContent extends AbstractContent { + protected final DirectoryContentProvider provider; + + public AbstractDirectoryContent(ProvidedSession session, DirectoryContentProvider provider) { + super(session); + this.provider = provider; + } + + abstract Dictionary doGetProperties(); + + @SuppressWarnings("unchecked") + @Override + public Optional get(QName key, Class clss) { + String attrName = key.getLocalPart(); + Object value = doGetProperties().get(attrName); + if (value == null) + return Optional.empty(); + // TODO deal with type and multiple + return Optional.of((A) value); + } + + @Override + protected Iterable keys() { + Dictionary properties = doGetProperties(); + Set keys = new TreeSet<>(NamespaceUtils.QNAME_COMPARATOR); + keys: for (Enumeration it = properties.keys(); it.hasMoreElements();) { + String key = it.nextElement(); + if (key.equalsIgnoreCase(LdapAttrs.objectClass.name())) + continue keys; + if (key.equalsIgnoreCase(LdapAttrs.objectClasses.name())) + continue keys; + ContentName name = new ContentName(CrName.LDAP_NAMESPACE_URI, key, provider); + keys.add(name); + } + return keys; + } + + @Override + public List getTypes() { + Dictionary properties = doGetProperties(); + List contentClasses = new ArrayList<>(); + String objectClass = properties.get(LdapAttrs.objectClass.name()).toString(); + contentClasses.add(new ContentName(CrName.LDAP_NAMESPACE_URI, objectClass, provider)); + + String[] objectClasses = properties.get(LdapAttrs.objectClasses.name()).toString().split("\\n"); + objectClasses: for (String oc : objectClasses) { + if (LdapObjs.top.name().equalsIgnoreCase(oc)) + continue objectClasses; + if (objectClass.equalsIgnoreCase(oc)) + continue objectClasses; + contentClasses.add(new ContentName(CrName.LDAP_NAMESPACE_URI, oc, provider)); + } + return contentClasses; + } + + @Override + public Object put(QName key, Object value) { + Object previous = get(key); + // TODO deal with typing + doGetProperties().put(key.getLocalPart(), value); + return previous; + } + + @Override + protected void removeAttr(QName key) { + doGetProperties().remove(key.getLocalPart()); + } + + @Override + public ContentProvider getProvider() { + return provider; + } + + +} diff --git a/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContent.java b/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContent.java index e1ad96077..4e738ae2b 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContent.java @@ -1,6 +1,7 @@ package org.argeo.cms.acr.directory; import java.util.ArrayList; +import java.util.Dictionary; import java.util.Iterator; import java.util.List; @@ -8,25 +9,21 @@ import javax.xml.namespace.QName; import org.argeo.api.acr.Content; import org.argeo.api.acr.ContentName; -import org.argeo.api.acr.spi.ContentProvider; import org.argeo.api.acr.spi.ProvidedSession; -import org.argeo.cms.acr.AbstractContent; import org.argeo.util.directory.Directory; import org.argeo.util.directory.HierarchyUnit; -class DirectoryContent extends AbstractContent { +class DirectoryContent extends AbstractDirectoryContent { private Directory directory; - private DirectoryContentProvider provider; public DirectoryContent(ProvidedSession session, DirectoryContentProvider provider, Directory directory) { - super(session); - this.provider = provider; + super(session, provider); this.directory = directory; } @Override - public ContentProvider getProvider() { - return provider; + Dictionary doGetProperties() { + return directory.getProperties(); } @Override 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 index f4afbdd53..f4a416aa7 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/directory/DirectoryContentProvider.java @@ -110,6 +110,9 @@ public class DirectoryContentProvider implements ContentProvider { return new UserManagerContent(session); } + /* + * COMMON UTILITIES + */ class UserManagerContent extends AbstractContent { public UserManagerContent(ProvidedSession session) { 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 index f6a0e3b52..9c1a480ba 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/directory/HierarchyUnitContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/directory/HierarchyUnitContent.java @@ -1,6 +1,7 @@ package org.argeo.cms.acr.directory; import java.util.ArrayList; +import java.util.Dictionary; import java.util.Iterator; import java.util.List; import java.util.Objects; @@ -10,29 +11,24 @@ 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.UserDirectory; import org.argeo.util.directory.HierarchyUnit; import org.osgi.service.useradmin.Role; -class HierarchyUnitContent extends AbstractContent { +class HierarchyUnitContent extends AbstractDirectoryContent { private HierarchyUnit hierarchyUnit; - private DirectoryContentProvider provider; - public HierarchyUnitContent(ProvidedSession session, DirectoryContentProvider provider, HierarchyUnit hierarchyUnit) { - super(session); + super(session, provider); Objects.requireNonNull(hierarchyUnit); - this.provider = provider; this.hierarchyUnit = hierarchyUnit; } @Override - public ContentProvider getProvider() { - return provider; + Dictionary doGetProperties() { + return hierarchyUnit.getProperties(); } @Override @@ -69,12 +65,19 @@ class HierarchyUnitContent extends AbstractContent { /* * TYPING */ - @Override public List getTypes() { - List res = new ArrayList<>(); - res.add(CrName.COLLECTION.get()); - return res; + List contentClasses = super.getTypes(); + contentClasses.add(CrName.COLLECTION.get()); + return contentClasses; + } + + @SuppressWarnings("unchecked") + @Override + public A adapt(Class clss) { + if (clss.equals(HierarchyUnit.class)) + return (A) hierarchyUnit; + return super.adapt(clss); } /* 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 index 2a22f023c..7aa144633 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/directory/RoleContent.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/directory/RoleContent.java @@ -1,45 +1,32 @@ 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 java.util.Dictionary; -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.UserDirectory; -import org.argeo.util.naming.LdapAttrs; -import org.argeo.util.naming.LdapObjs; +import org.osgi.service.useradmin.Group; import org.osgi.service.useradmin.Role; import org.osgi.service.useradmin.User; -class RoleContent extends AbstractContent { +class RoleContent extends AbstractDirectoryContent { - private DirectoryContentProvider provider; private HierarchyUnitContent parent; private Role role; public RoleContent(ProvidedSession session, DirectoryContentProvider provider, HierarchyUnitContent parent, Role role) { - super(session); - this.provider = provider; + super(session, provider); this.parent = parent; this.role = role; } @Override - public ContentProvider getProvider() { - return provider; + Dictionary doGetProperties() { + return role.getProperties(); } @Override @@ -53,60 +40,6 @@ class RoleContent extends AbstractContent { 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<>(); - String objectClass = role.getProperties().get(LdapAttrs.objectClass.name()).toString(); - contentClasses.add(new ContentName(CrName.LDAP_NAMESPACE_URI, objectClass, provider)); - - String[] objectClasses = role.getProperties().get(LdapAttrs.objectClasses.name()).toString().split("\\n"); - objectClasses: for (String oc : objectClasses) { - if (LdapObjs.top.name().equalsIgnoreCase(oc)) - continue objectClasses; - if (objectClass.equalsIgnoreCase(oc)) - continue objectClasses; - contentClasses.add(new ContentName(CrName.LDAP_NAMESPACE_URI, oc, provider)); - } - 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) { diff --git a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminUtils.java b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminUtils.java index eed38cc32..5a3657211 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminUtils.java @@ -67,16 +67,17 @@ public class UserAdminUtils { */ public static String getUserDisplayName(UserAdmin userAdmin, String dn) { Role user = userAdmin.getRole(dn); - String dName; if (user == null) - dName = getUserLocalId(dn); - else { - dName = getProperty(user, LdapAttrs.displayName.name()); - if (isEmpty(dName)) - dName = getProperty(user, LdapAttrs.cn.name()); - if (isEmpty(dName)) - dName = getUserLocalId(dn); - } + return getUserLocalId(dn); + return getUserDisplayName(user); + } + + public static String getUserDisplayName(Role user) { + String dName = getProperty(user, LdapAttrs.displayName.name()); + if (isEmpty(dName)) + dName = getProperty(user, LdapAttrs.cn.name()); + if (isEmpty(dName)) + dName = getUserLocalId(user.getName()); return dName; } diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUserAdmin.java b/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUserAdmin.java index 9f6d62d7a..6f12195dc 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUserAdmin.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUserAdmin.java @@ -30,7 +30,6 @@ import org.argeo.util.directory.ldap.LdapEntry; import org.argeo.util.directory.ldap.LdapEntryWorkingCopy; import org.argeo.util.directory.ldap.LdapNameUtils; import org.argeo.util.directory.ldap.LdifDao; -import org.argeo.util.naming.LdapObjs; import org.osgi.framework.Filter; import org.osgi.framework.FrameworkUtil; import org.osgi.framework.InvalidSyntaxException; @@ -74,7 +73,7 @@ public class DirectoryUserAdmin extends AbstractLdapDirectory implements UserAdm String username = (String) credentials.get(SHARED_STATE_USERNAME); if (username == null) username = user.getName(); - Dictionary properties = cloneProperties(); + Dictionary properties = cloneConfigProperties(); properties.put(Context.SECURITY_PRINCIPAL, username.toString()); Object pwdCred = credentials.get(SHARED_STATE_PASSWORD); byte[] pwd = (byte[]) pwdCred; @@ -102,7 +101,7 @@ public class DirectoryUserAdmin extends AbstractLdapDirectory implements UserAdm } else { throw new IllegalStateException("Password is required"); } - Dictionary properties = cloneProperties(); + Dictionary properties = cloneConfigProperties(); properties.put(DirectoryConf.readOnly.name(), "true"); DirectoryUserAdmin scopedUserAdmin = new DirectoryUserAdmin(null, properties, true); // scopedUserAdmin.groups = Collections.unmodifiableNavigableMap(groups); @@ -283,7 +282,7 @@ public class DirectoryUserAdmin extends AbstractLdapDirectory implements UserAdm checkEdit(); LdapEntryWorkingCopy wc = getWorkingCopy(); LdapName dn = toLdapName(name); - if ((getDirectoryDao().daoHasEntry(dn) && !wc.getDeletedData().containsKey(dn)) + if ((getDirectoryDao().entryExists(dn) && !wc.getDeletedData().containsKey(dn)) || wc.getNewData().containsKey(dn)) throw new IllegalArgumentException("Already a role " + name); BasicAttributes attrs = new BasicAttributes(true); @@ -380,17 +379,11 @@ public class DirectoryUserAdmin extends AbstractLdapDirectory implements UserAdm */ protected LdapEntry newUser(LdapName name, Attributes attrs) { // TODO support devices, applications, etc. - return new LdifUser.LdifPerson(this, name, attrs); + return new LdifUser(this, name, attrs); } protected LdapEntry newGroup(LdapName name, Attributes attrs) { - if (LdapNameUtils.getParentRdn(name).equals(getSystemRoleBaseRdn())) - return new LdifGroup.LdifSystemPermissions(this, name, attrs); - - if (hasObjectClass(attrs, LdapObjs.organization)) - return new LdifGroup.LdifOrganization(this, name, attrs); - else - return new LdifGroup.LdifFunctionalGroup(this, name, attrs); + return new LdifGroup(this, name, attrs); } diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifGroup.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifGroup.java index 7aad15a8c..2453fcb23 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifGroup.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifGroup.java @@ -8,14 +8,11 @@ import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.ldap.LdapName; -import org.argeo.util.directory.FunctionalGroup; -import org.argeo.util.directory.Organization; -import org.argeo.util.directory.SystemPermissions; import org.argeo.util.directory.ldap.AbstractLdapDirectory; import org.osgi.service.useradmin.Role; /** Directory group implementation */ -abstract class LdifGroup extends LdifUser implements DirectoryGroup { +class LdifGroup extends LdifUser implements DirectoryGroup { private final String memberAttributeId; LdifGroup(AbstractLdapDirectory userAdmin, LdapName dn, Attributes attributes) { @@ -125,30 +122,7 @@ abstract class LdifGroup extends LdifUser implements DirectoryGroup { return GROUP; } - /* - * KIND - */ - static class LdifFunctionalGroup extends LdifGroup implements FunctionalGroup { - - public LdifFunctionalGroup(DirectoryUserAdmin userAdmin, LdapName dn, Attributes attributes) { - super(userAdmin, dn, attributes); - } - - } - - static class LdifOrganization extends LdifGroup implements Organization { - - public LdifOrganization(DirectoryUserAdmin userAdmin, LdapName dn, Attributes attributes) { - super(userAdmin, dn, attributes); - } - - } - - static class LdifSystemPermissions extends LdifGroup implements SystemPermissions { - - public LdifSystemPermissions(DirectoryUserAdmin userAdmin, LdapName dn, Attributes attributes) { - super(userAdmin, dn, attributes); - } - + protected DirectoryUserAdmin getUserAdmin() { + return (DirectoryUserAdmin) getDirectory(); } } 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 cceb6e461..11de4ed56 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java @@ -1,44 +1,17 @@ package org.argeo.osgi.useradmin; -import static java.nio.charset.StandardCharsets.US_ASCII; - -import java.math.BigInteger; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Base64; -import java.util.Collections; import java.util.Dictionary; -import java.util.Enumeration; -import java.util.Iterator; -import java.util.List; -import java.util.StringJoiner; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; -import javax.naming.directory.BasicAttribute; import javax.naming.ldap.LdapName; -import org.argeo.util.directory.DirectoryDigestUtils; -import org.argeo.util.directory.Person; import org.argeo.util.directory.ldap.AbstractLdapDirectory; -import org.argeo.util.directory.ldap.AbstractLdapEntry; -import org.argeo.util.directory.ldap.AuthPassword; -import org.argeo.util.naming.LdapAttrs; -import org.argeo.util.naming.LdapObjs; -import org.argeo.util.naming.SharedSecret; +import org.argeo.util.directory.ldap.DefaultLdapEntry; /** Directory user implementation */ -abstract class LdifUser extends AbstractLdapEntry implements DirectoryUser { - private final AttributeDictionary properties; - private final AttributeDictionary credentials; - +class LdifUser extends DefaultLdapEntry implements DirectoryUser { LdifUser(AbstractLdapDirectory userAdmin, LdapName dn, Attributes attributes) { super(userAdmin, dn, attributes); - properties = new AttributeDictionary(false); - credentials = new AttributeDictionary(true); } @Override @@ -51,316 +24,8 @@ abstract class LdifUser extends AbstractLdapEntry implements DirectoryUser { return USER; } - @Override - public Dictionary getProperties() { - return properties; - } - @Override public Dictionary getCredentials() { return credentials; } - - @Override - public boolean hasCredential(String key, Object value) { - if (key == null) { - // TODO check other sources (like PKCS12) - // String pwd = new String((char[]) value); - // authPassword (RFC 312 https://tools.ietf.org/html/rfc3112) - char[] password = DirectoryDigestUtils.bytesToChars(value); - - if (getDirectory().getForcedPassword() != null - && getDirectory().getForcedPassword().equals(new String(password))) - return true; - - AuthPassword authPassword = AuthPassword.matchAuthValue(getAttributes(), password); - if (authPassword != null) { - if (authPassword.getAuthScheme().equals(SharedSecret.X_SHARED_SECRET)) { - SharedSecret onceToken = new SharedSecret(authPassword); - if (onceToken.isExpired()) { - // AuthPassword.remove(getAttributes(), onceToken); - return false; - } else { - // boolean wasRemoved = AuthPassword.remove(getAttributes(), onceToken); - return true; - } - // TODO delete expired tokens? - } else { - // TODO implement SHA - throw new UnsupportedOperationException( - "Unsupported authPassword scheme " + authPassword.getAuthScheme()); - } - } - - // Regular password -// byte[] hashedPassword = hash(password, DigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256); - if (hasCredential(LdapAttrs.userPassword.name(), DirectoryDigestUtils.charsToBytes(password))) - return true; - return false; - } - - Object storedValue = getCredentials().get(key); - if (storedValue == null || value == null) - return false; - if (!(value instanceof String || value instanceof byte[])) - return false; - if (storedValue instanceof String && value instanceof String) - return storedValue.equals(value); - if (storedValue instanceof byte[] && value instanceof byte[]) { - String storedBase64 = new String((byte[]) storedValue, US_ASCII); - String passwordScheme = null; - if (storedBase64.charAt(0) == '{') { - int index = storedBase64.indexOf('}'); - if (index > 0) { - passwordScheme = storedBase64.substring(1, index); - String storedValueBase64 = storedBase64.substring(index + 1); - byte[] storedValueBytes = Base64.getDecoder().decode(storedValueBase64); - char[] passwordValue = DirectoryDigestUtils.bytesToChars((byte[]) value); - byte[] valueBytes; - if (DirectoryDigestUtils.PASSWORD_SCHEME_SHA.equals(passwordScheme)) { - valueBytes = DirectoryDigestUtils.toPasswordScheme(passwordScheme, passwordValue, null, null, - null); - } else if (DirectoryDigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256.equals(passwordScheme)) { - // see https://www.thesubtlety.com/post/a-389-ds-pbkdf2-password-checker/ - byte[] iterationsArr = Arrays.copyOfRange(storedValueBytes, 0, 4); - BigInteger iterations = new BigInteger(iterationsArr); - byte[] salt = Arrays.copyOfRange(storedValueBytes, iterationsArr.length, - iterationsArr.length + 64); - byte[] keyArr = Arrays.copyOfRange(storedValueBytes, iterationsArr.length + salt.length, - storedValueBytes.length); - int keyLengthBits = keyArr.length * 8; - valueBytes = DirectoryDigestUtils.toPasswordScheme(passwordScheme, passwordValue, salt, - iterations.intValue(), keyLengthBits); - } else { - throw new UnsupportedOperationException("Unknown password scheme " + passwordScheme); - } - return Arrays.equals(storedValueBytes, valueBytes); - } - } - } -// if (storedValue instanceof byte[] && value instanceof byte[]) { -// return Arrays.equals((byte[]) storedValue, (byte[]) value); -// } - return false; - } - - /** Hash the password */ - byte[] sha1hash(char[] password) { - byte[] hashedPassword = ("{SHA}" + Base64.getEncoder() - .encodeToString(DirectoryDigestUtils.sha1(DirectoryDigestUtils.charsToBytes(password)))) - .getBytes(StandardCharsets.UTF_8); - return hashedPassword; - } - -// byte[] hash(char[] password, String passwordScheme) { -// if (passwordScheme == null) -// passwordScheme = DigestUtils.PASSWORD_SCHEME_SHA; -// byte[] hashedPassword = ("{" + passwordScheme + "}" -// + Base64.getEncoder().encodeToString(DigestUtils.toPasswordScheme(passwordScheme, password))) -// .getBytes(US_ASCII); -// return hashedPassword; -// } - - protected DirectoryUserAdmin getUserAdmin() { - return (DirectoryUserAdmin) getDirectory(); - } - - private class AttributeDictionary extends Dictionary { - private final List effectiveKeys = new ArrayList(); - private final List attrFilter; - private final Boolean includeFilter; - - public AttributeDictionary(Boolean credentials) { - this.attrFilter = getDirectory().getCredentialAttributeIds(); - this.includeFilter = credentials; - try { - NamingEnumeration ids = getAttributes().getIDs(); - while (ids.hasMore()) { - String id = ids.next(); - if (credentials && attrFilter.contains(id)) - effectiveKeys.add(id); - else if (!credentials && !attrFilter.contains(id)) - effectiveKeys.add(id); - } - } catch (NamingException e) { - throw new IllegalStateException("Cannot initialise attribute dictionary", e); - } - if (!credentials) - effectiveKeys.add(LdapAttrs.objectClasses.name()); - } - - @Override - public int size() { - return effectiveKeys.size(); - } - - @Override - public boolean isEmpty() { - return effectiveKeys.size() == 0; - } - - @Override - public Enumeration keys() { - return Collections.enumeration(effectiveKeys); - } - - @Override - public Enumeration elements() { - final Iterator it = effectiveKeys.iterator(); - return new Enumeration() { - - @Override - public boolean hasMoreElements() { - return it.hasNext(); - } - - @Override - public Object nextElement() { - String key = it.next(); - return get(key); - } - - }; - } - - @Override - public Object get(Object key) { - try { - Attribute attr = !key.equals(LdapAttrs.objectClasses.name()) ? getAttributes().get(key.toString()) - : getAttributes().get(LdapAttrs.objectClass.name()); - if (attr == null) - return null; - Object value = attr.get(); - if (value instanceof byte[]) { - if (key.equals(LdapAttrs.userPassword.name())) - // TODO other cases (certificates, images) - return value; - value = new String((byte[]) value, StandardCharsets.UTF_8); - } - if (attr.size() == 1) - return value; - // special case for object class - if (key.equals(LdapAttrs.objectClass.name())) { - // TODO support multiple object classes - NamingEnumeration en = attr.getAll(); - String first = null; - attrs: while (en.hasMore()) { - String v = en.next().toString(); - if (v.equalsIgnoreCase(LdapObjs.top.name())) - continue attrs; - if (first == null) - first = v; - if (v.equalsIgnoreCase(getDirectory().getUserObjectClass())) - return getDirectory().getUserObjectClass(); - else if (v.equalsIgnoreCase(getDirectory().getGroupObjectClass())) - return getDirectory().getGroupObjectClass(); - } - if (first != null) - return first; - throw new IllegalStateException("Cannot find objectClass in " + value); - } else { - NamingEnumeration en = attr.getAll(); - StringJoiner values = new StringJoiner("\n"); - while (en.hasMore()) { - String v = en.next().toString(); - values.add(v); - } - return values.toString(); - } -// else -// return value; - } catch (NamingException e) { - throw new IllegalStateException("Cannot get value for attribute " + key, e); - } - } - - @Override - public Object put(String key, Object value) { - if (key == null) { - // TODO persist to other sources (like PKCS12) - char[] password = DirectoryDigestUtils.bytesToChars(value); - byte[] hashedPassword = sha1hash(password); - return put(LdapAttrs.userPassword.name(), hashedPassword); - } - if (key.startsWith("X-")) { - return put(LdapAttrs.authPassword.name(), value); - } - - getDirectory().checkEdit(); - if (!isEditing()) - startEditing(); - - if (!(value instanceof String || value instanceof byte[])) - throw new IllegalArgumentException("Value must be String or byte[]"); - - if (includeFilter && !attrFilter.contains(key)) - throw new IllegalArgumentException("Key " + key + " not included"); - else if (!includeFilter && attrFilter.contains(key)) - throw new IllegalArgumentException("Key " + key + " excluded"); - - try { - Attribute attribute = getModifiedAttributes().get(key.toString()); - // if (attribute == null) // block unit tests - attribute = new BasicAttribute(key.toString()); - if (value instanceof String && !isAsciiPrintable(((String) value))) - attribute.add(((String) value).getBytes(StandardCharsets.UTF_8)); - else - attribute.add(value); - Attribute previousAttribute = getModifiedAttributes().put(attribute); - if (previousAttribute != null) - return previousAttribute.get(); - else - return null; - } catch (NamingException e) { - throw new IllegalStateException("Cannot get value for attribute " + key, e); - } - } - - @Override - public Object remove(Object key) { - getDirectory().checkEdit(); - if (!isEditing()) - startEditing(); - - if (includeFilter && !attrFilter.contains(key)) - throw new IllegalArgumentException("Key " + key + " not included"); - else if (!includeFilter && attrFilter.contains(key)) - throw new IllegalArgumentException("Key " + key + " excluded"); - - try { - Attribute attr = getModifiedAttributes().remove(key.toString()); - if (attr != null) - return attr.get(); - else - return null; - } catch (NamingException e) { - throw new IllegalStateException("Cannot remove attribute " + key, e); - } - } - } - - private static boolean isAsciiPrintable(String str) { - if (str == null) { - return false; - } - int sz = str.length(); - for (int i = 0; i < sz; i++) { - if (isAsciiPrintable(str.charAt(i)) == false) { - return false; - } - } - return true; - } - - private static boolean isAsciiPrintable(char ch) { - return ch >= 32 && ch < 127; - } - - static class LdifPerson extends LdifUser implements Person { - - public LdifPerson(DirectoryUserAdmin userAdmin, LdapName dn, Attributes attributes) { - super(userAdmin, dn, attributes); - } - - } } 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 1adc7e0df..c052fee1b 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java @@ -41,12 +41,12 @@ public class OsUserDirectory extends AbstractLdapDirectoryDao { } @Override - public Boolean daoHasEntry(LdapName dn) { + public Boolean entryExists(LdapName dn) { return osUserDn.equals(dn); } @Override - public LdapEntry daoGetEntry(LdapName key) throws NameNotFoundException { + public LdapEntry doGetEntry(LdapName key) throws NameNotFoundException { if (osUserDn.equals(key)) return osUser; else diff --git a/org.argeo.util/src/org/argeo/util/directory/Directory.java b/org.argeo.util/src/org/argeo/util/directory/Directory.java index 05808908d..0bbb5e53f 100644 --- a/org.argeo.util/src/org/argeo/util/directory/Directory.java +++ b/org.argeo.util/src/org/argeo/util/directory/Directory.java @@ -1,5 +1,6 @@ package org.argeo.util.directory; +import java.util.Dictionary; import java.util.Optional; import org.argeo.util.transaction.WorkControl; @@ -21,6 +22,11 @@ public interface Directory { void setTransactionControl(WorkControl transactionControl); + /* + * METADATA + */ + public Dictionary getProperties(); + /* * HIERARCHY */ diff --git a/org.argeo.util/src/org/argeo/util/directory/FunctionalGroup.java b/org.argeo.util/src/org/argeo/util/directory/FunctionalGroup.java deleted file mode 100644 index 89511aba5..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/FunctionalGroup.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.argeo.util.directory; - -public interface FunctionalGroup { - -} diff --git a/org.argeo.util/src/org/argeo/util/directory/HierarchyUnit.java b/org.argeo.util/src/org/argeo/util/directory/HierarchyUnit.java index 0194ffc89..68cc1bb2d 100644 --- a/org.argeo.util/src/org/argeo/util/directory/HierarchyUnit.java +++ b/org.argeo.util/src/org/argeo/util/directory/HierarchyUnit.java @@ -1,5 +1,7 @@ package org.argeo.util.directory; +import java.util.Dictionary; + /** A unit within the high-level organisational structure of a directory. */ public interface HierarchyUnit { String getHierarchyUnitName(); @@ -14,5 +16,5 @@ public interface HierarchyUnit { Directory getDirectory(); -// Map getHierarchyProperties(); + Dictionary getProperties(); } diff --git a/org.argeo.util/src/org/argeo/util/directory/Organization.java b/org.argeo.util/src/org/argeo/util/directory/Organization.java deleted file mode 100644 index bbbdcd923..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/Organization.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.argeo.util.directory; - -public interface Organization { - -} diff --git a/org.argeo.util/src/org/argeo/util/directory/Person.java b/org.argeo.util/src/org/argeo/util/directory/Person.java deleted file mode 100644 index d782ee481..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/Person.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.argeo.util.directory; - -public interface Person { - -} diff --git a/org.argeo.util/src/org/argeo/util/directory/SystemPermissions.java b/org.argeo.util/src/org/argeo/util/directory/SystemPermissions.java deleted file mode 100644 index 3ab16b898..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/SystemPermissions.java +++ /dev/null @@ -1,5 +0,0 @@ -package org.argeo.util.directory; - -public interface SystemPermissions { - -} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectory.java b/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectory.java index d8e8e7d21..eab82e0ec 100644 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectory.java +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapDirectory.java @@ -38,7 +38,7 @@ public abstract class AbstractLdapDirectory implements Directory, XAResourceProv protected static final String SHARED_STATE_PASSWORD = "javax.security.auth.login.password"; protected final LdapName baseDn; - protected final Hashtable properties; + protected final Hashtable configProperties; private final Rdn userBaseRdn, groupBaseRdn, systemRoleBaseRdn; private final String userObjectClass, groupObjectClass; private String memberAttributeId = "member"; @@ -60,33 +60,33 @@ public abstract class AbstractLdapDirectory implements Directory, XAResourceProv private LdapDirectoryDao directoryDao; public AbstractLdapDirectory(URI uriArg, Dictionary props, boolean scoped) { - this.properties = new Hashtable(); + this.configProperties = new Hashtable(); for (Enumeration keys = props.keys(); keys.hasMoreElements();) { String key = keys.nextElement(); - properties.put(key, props.get(key)); + configProperties.put(key, props.get(key)); } - baseDn = toLdapName(DirectoryConf.baseDn.getValue(properties)); + baseDn = toLdapName(DirectoryConf.baseDn.getValue(configProperties)); this.scoped = scoped; if (uriArg != null) { uri = uriArg.toString(); // uri from properties is ignored } else { - String uriStr = DirectoryConf.uri.getValue(properties); + String uriStr = DirectoryConf.uri.getValue(configProperties); if (uriStr == null) uri = null; else uri = uriStr; } - forcedPassword = DirectoryConf.forcedPassword.getValue(properties); + forcedPassword = DirectoryConf.forcedPassword.getValue(configProperties); - userObjectClass = DirectoryConf.userObjectClass.getValue(properties); - groupObjectClass = DirectoryConf.groupObjectClass.getValue(properties); + userObjectClass = DirectoryConf.userObjectClass.getValue(configProperties); + groupObjectClass = DirectoryConf.groupObjectClass.getValue(configProperties); - String userBase = DirectoryConf.userBase.getValue(properties); - String groupBase = DirectoryConf.groupBase.getValue(properties); - String systemRoleBase = DirectoryConf.systemRoleBase.getValue(properties); + String userBase = DirectoryConf.userBase.getValue(configProperties); + String groupBase = DirectoryConf.groupBase.getValue(configProperties); + String systemRoleBase = DirectoryConf.systemRoleBase.getValue(configProperties); try { // baseDn = new LdapName(UserAdminConf.baseDn.getValue(properties)); userBaseRdn = new Rdn(userBase); @@ -95,20 +95,20 @@ public abstract class AbstractLdapDirectory implements Directory, XAResourceProv // groupBaseDn = new LdapName(groupBase + "," + baseDn); systemRoleBaseRdn = new Rdn(systemRoleBase); } catch (InvalidNameException e) { - throw new IllegalArgumentException("Badly formated base DN " + DirectoryConf.baseDn.getValue(properties), - e); + throw new IllegalArgumentException( + "Badly formated base DN " + DirectoryConf.baseDn.getValue(configProperties), e); } // read only - String readOnlyStr = DirectoryConf.readOnly.getValue(properties); + String readOnlyStr = DirectoryConf.readOnly.getValue(configProperties); if (readOnlyStr == null) { readOnly = readOnlyDefault(uri); - properties.put(DirectoryConf.readOnly.name(), Boolean.toString(readOnly)); + configProperties.put(DirectoryConf.readOnly.name(), Boolean.toString(readOnly)); } else readOnly = Boolean.parseBoolean(readOnlyStr); // disabled - String disabledStr = DirectoryConf.disabled.getValue(properties); + String disabledStr = DirectoryConf.disabled.getValue(configProperties); if (disabledStr != null) disabled = Boolean.parseBoolean(disabledStr); else @@ -202,7 +202,7 @@ public abstract class AbstractLdapDirectory implements Directory, XAResourceProv checkEdit(); LdapEntryWorkingCopy wc = getWorkingCopy(); boolean actuallyDeleted; - if (getDirectoryDao().daoHasEntry(dn) || wc.getNewData().containsKey(dn)) { + if (getDirectoryDao().entryExists(dn) || wc.getNewData().containsKey(dn)) { LdapEntry user = doGetRole(dn); wc.getDeletedData().put(dn, user); actuallyDeleted = true; @@ -224,7 +224,7 @@ public abstract class AbstractLdapDirectory implements Directory, XAResourceProv LdapEntryWorkingCopy wc = getWorkingCopy(); LdapEntry user; try { - user = getDirectoryDao().daoGetEntry(dn); + user = getDirectoryDao().doGetEntry(dn); } catch (NameNotFoundException e) { user = null; } @@ -386,12 +386,27 @@ public abstract class AbstractLdapDirectory implements Directory, XAResourceProv return true;// read only by default } + /* + * AS AN ENTRY + */ + public LdapEntry asLdapEntry() { + try { + return directoryDao.doGetEntry(baseDn); + } catch (NameNotFoundException e) { + throw new IllegalStateException("Cannot get " + baseDn + " entry", e); + } + } + + public Dictionary getProperties() { + return asLdapEntry().getProperties(); + } + /* * ACCESSORS */ @Override public Optional getRealm() { - Object realm = getProperties().get(DirectoryConf.realm.name()); + Object realm = configProperties.get(DirectoryConf.realm.name()); if (realm == null) return Optional.empty(); return Optional.of(realm.toString()); @@ -421,12 +436,12 @@ public abstract class AbstractLdapDirectory implements Directory, XAResourceProv return systemRoleBaseRdn; } - public Dictionary getProperties() { - return properties; - } +// public Dictionary getConfigProperties() { +// return configProperties; +// } - public Dictionary cloneProperties() { - return new Hashtable<>(properties); + public Dictionary cloneConfigProperties() { + return new Hashtable<>(configProperties); } public String getForcedPassword() { diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapEntry.java b/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapEntry.java deleted file mode 100644 index 25f233a01..000000000 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/AbstractLdapEntry.java +++ /dev/null @@ -1,114 +0,0 @@ -package org.argeo.util.directory.ldap; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.ldap.LdapName; - -/** An entry in an LDAP (or LDIF) directory. */ -public abstract class AbstractLdapEntry implements LdapEntry { - private final AbstractLdapDirectory directory; - - private final LdapName dn; - - private Attributes publishedAttributes; - - protected AbstractLdapEntry(AbstractLdapDirectory directory, LdapName dn, Attributes attributes) { - Objects.requireNonNull(directory); - Objects.requireNonNull(dn); - this.directory = directory; - this.dn = dn; - this.publishedAttributes = attributes; - } - - @Override - public LdapName getDn() { - return dn; - } - - public synchronized Attributes getAttributes() { - return isEditing() ? getModifiedAttributes() : publishedAttributes; - } - - @Override - public List getReferences(String attributeId){ - Attribute memberAttribute = getAttributes().get(attributeId); - if (memberAttribute == null) - return new ArrayList(); - try { - List roles = new ArrayList(); - NamingEnumeration values = memberAttribute.getAll(); - while (values.hasMore()) { - LdapName dn = new LdapName(values.next().toString()); - roles.add(dn); - } - return roles; - } catch (NamingException e) { - throw new IllegalStateException("Cannot get members", e); - } - - } - - /** Should only be called from working copy thread. */ - protected synchronized Attributes getModifiedAttributes() { - assert getWc() != null; - return getWc().getModifiedData().get(getDn()); - } - - protected synchronized boolean isEditing() { - return getWc() != null && getModifiedAttributes() != null; - } - - private synchronized LdapEntryWorkingCopy getWc() { - return directory.getWorkingCopy(); - } - - protected synchronized void startEditing() { -// if (frozen) -// throw new IllegalStateException("Cannot edit frozen view"); - if (directory.isReadOnly()) - throw new IllegalStateException("User directory is read-only"); - assert getModifiedAttributes() == null; - getWc().startEditing(this); - // modifiedAttributes = (Attributes) publishedAttributes.clone(); - } - - public synchronized void publishAttributes(Attributes modifiedAttributes) { - publishedAttributes = modifiedAttributes; - } - - public AbstractLdapDirectory getDirectory() { - return directory; - } - - public LdapDirectoryDao getDirectoryDao() { - return directory.getDirectoryDao(); - } - - @Override - public int hashCode() { - return dn.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj instanceof LdapEntry) { - LdapEntry that = (LdapEntry) obj; - return this.dn.equals(that.getDn()); - } - return false; - } - - @Override - public String toString() { - return dn.toString(); - } - -} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/DefaultLdapEntry.java b/org.argeo.util/src/org/argeo/util/directory/ldap/DefaultLdapEntry.java new file mode 100644 index 000000000..8eff66900 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/DefaultLdapEntry.java @@ -0,0 +1,431 @@ +package org.argeo.util.directory.ldap; + +import static java.nio.charset.StandardCharsets.US_ASCII; + +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttribute; +import javax.naming.ldap.LdapName; + +import org.argeo.util.directory.DirectoryDigestUtils; +import org.argeo.util.naming.LdapAttrs; +import org.argeo.util.naming.LdapObjs; +import org.argeo.util.naming.SharedSecret; + +/** An entry in an LDAP (or LDIF) directory. */ +public class DefaultLdapEntry implements LdapEntry { + private final AbstractLdapDirectory directory; + + private final LdapName dn; + + private Attributes publishedAttributes; + + // Temporarily expose the fields + protected final AttributeDictionary properties; + protected final AttributeDictionary credentials; + + protected DefaultLdapEntry(AbstractLdapDirectory directory, LdapName dn, Attributes attributes) { + Objects.requireNonNull(directory); + Objects.requireNonNull(dn); + this.directory = directory; + this.dn = dn; + this.publishedAttributes = attributes; + properties = new AttributeDictionary(false); + credentials = new AttributeDictionary(true); + } + + @Override + public LdapName getDn() { + return dn; + } + + public synchronized Attributes getAttributes() { + return isEditing() ? getModifiedAttributes() : publishedAttributes; + } + + @Override + public List getReferences(String attributeId) { + Attribute memberAttribute = getAttributes().get(attributeId); + if (memberAttribute == null) + return new ArrayList(); + try { + List roles = new ArrayList(); + NamingEnumeration values = memberAttribute.getAll(); + while (values.hasMore()) { + LdapName dn = new LdapName(values.next().toString()); + roles.add(dn); + } + return roles; + } catch (NamingException e) { + throw new IllegalStateException("Cannot get members", e); + } + + } + + /** Should only be called from working copy thread. */ + protected synchronized Attributes getModifiedAttributes() { + assert getWc() != null; + return getWc().getModifiedData().get(getDn()); + } + + protected synchronized boolean isEditing() { + return getWc() != null && getModifiedAttributes() != null; + } + + private synchronized LdapEntryWorkingCopy getWc() { + return directory.getWorkingCopy(); + } + + protected synchronized void startEditing() { +// if (frozen) +// throw new IllegalStateException("Cannot edit frozen view"); + if (directory.isReadOnly()) + throw new IllegalStateException("User directory is read-only"); + assert getModifiedAttributes() == null; + getWc().startEditing(this); + // modifiedAttributes = (Attributes) publishedAttributes.clone(); + } + + public synchronized void publishAttributes(Attributes modifiedAttributes) { + publishedAttributes = modifiedAttributes; + } + + /* + * PROPERTIES + */ + @Override + public Dictionary getProperties() { + return properties; + } + + /* + * CREDENTIALS + */ + @Override + public boolean hasCredential(String key, Object value) { + if (key == null) { + // TODO check other sources (like PKCS12) + // String pwd = new String((char[]) value); + // authPassword (RFC 312 https://tools.ietf.org/html/rfc3112) + char[] password = DirectoryDigestUtils.bytesToChars(value); + + if (getDirectory().getForcedPassword() != null + && getDirectory().getForcedPassword().equals(new String(password))) + return true; + + AuthPassword authPassword = AuthPassword.matchAuthValue(getAttributes(), password); + if (authPassword != null) { + if (authPassword.getAuthScheme().equals(SharedSecret.X_SHARED_SECRET)) { + SharedSecret onceToken = new SharedSecret(authPassword); + if (onceToken.isExpired()) { + // AuthPassword.remove(getAttributes(), onceToken); + return false; + } else { + // boolean wasRemoved = AuthPassword.remove(getAttributes(), onceToken); + return true; + } + // TODO delete expired tokens? + } else { + // TODO implement SHA + throw new UnsupportedOperationException( + "Unsupported authPassword scheme " + authPassword.getAuthScheme()); + } + } + + // Regular password +// byte[] hashedPassword = hash(password, DigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256); + if (hasCredential(LdapAttrs.userPassword.name(), DirectoryDigestUtils.charsToBytes(password))) + return true; + return false; + } + + Object storedValue = credentials.get(key); + if (storedValue == null || value == null) + return false; + if (!(value instanceof String || value instanceof byte[])) + return false; + if (storedValue instanceof String && value instanceof String) + return storedValue.equals(value); + if (storedValue instanceof byte[] && value instanceof byte[]) { + String storedBase64 = new String((byte[]) storedValue, US_ASCII); + String passwordScheme = null; + if (storedBase64.charAt(0) == '{') { + int index = storedBase64.indexOf('}'); + if (index > 0) { + passwordScheme = storedBase64.substring(1, index); + String storedValueBase64 = storedBase64.substring(index + 1); + byte[] storedValueBytes = Base64.getDecoder().decode(storedValueBase64); + char[] passwordValue = DirectoryDigestUtils.bytesToChars((byte[]) value); + byte[] valueBytes; + if (DirectoryDigestUtils.PASSWORD_SCHEME_SHA.equals(passwordScheme)) { + valueBytes = DirectoryDigestUtils.toPasswordScheme(passwordScheme, passwordValue, null, null, + null); + } else if (DirectoryDigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256.equals(passwordScheme)) { + // see https://www.thesubtlety.com/post/a-389-ds-pbkdf2-password-checker/ + byte[] iterationsArr = Arrays.copyOfRange(storedValueBytes, 0, 4); + BigInteger iterations = new BigInteger(iterationsArr); + byte[] salt = Arrays.copyOfRange(storedValueBytes, iterationsArr.length, + iterationsArr.length + 64); + byte[] keyArr = Arrays.copyOfRange(storedValueBytes, iterationsArr.length + salt.length, + storedValueBytes.length); + int keyLengthBits = keyArr.length * 8; + valueBytes = DirectoryDigestUtils.toPasswordScheme(passwordScheme, passwordValue, salt, + iterations.intValue(), keyLengthBits); + } else { + throw new UnsupportedOperationException("Unknown password scheme " + passwordScheme); + } + return Arrays.equals(storedValueBytes, valueBytes); + } + } + } +// if (storedValue instanceof byte[] && value instanceof byte[]) { +// return Arrays.equals((byte[]) storedValue, (byte[]) value); +// } + return false; + } + + /** Hash the password */ + private static byte[] sha1hash(char[] password) { + byte[] hashedPassword = ("{SHA}" + Base64.getEncoder() + .encodeToString(DirectoryDigestUtils.sha1(DirectoryDigestUtils.charsToBytes(password)))) + .getBytes(StandardCharsets.UTF_8); + return hashedPassword; + } + + public AbstractLdapDirectory getDirectory() { + return directory; + } + + public LdapDirectoryDao getDirectoryDao() { + return directory.getDirectoryDao(); + } + + @Override + public int hashCode() { + return dn.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj instanceof LdapEntry) { + LdapEntry that = (LdapEntry) obj; + return this.dn.equals(that.getDn()); + } + return false; + } + + @Override + public String toString() { + return dn.toString(); + } + + private static boolean isAsciiPrintable(String str) { + if (str == null) { + return false; + } + int sz = str.length(); + for (int i = 0; i < sz; i++) { + if (isAsciiPrintable(str.charAt(i)) == false) { + return false; + } + } + return true; + } + + private static boolean isAsciiPrintable(char ch) { + return ch >= 32 && ch < 127; + } + + protected class AttributeDictionary extends Dictionary { + private final List effectiveKeys = new ArrayList(); + private final List attrFilter; + private final Boolean includeFilter; + + public AttributeDictionary(Boolean credentials) { + this.attrFilter = getDirectory().getCredentialAttributeIds(); + this.includeFilter = credentials; + try { + NamingEnumeration ids = getAttributes().getIDs(); + while (ids.hasMore()) { + String id = ids.next(); + if (credentials && attrFilter.contains(id)) + effectiveKeys.add(id); + else if (!credentials && !attrFilter.contains(id)) + effectiveKeys.add(id); + } + } catch (NamingException e) { + throw new IllegalStateException("Cannot initialise attribute dictionary", e); + } + if (!credentials) + effectiveKeys.add(LdapAttrs.objectClasses.name()); + } + + @Override + public int size() { + return effectiveKeys.size(); + } + + @Override + public boolean isEmpty() { + return effectiveKeys.size() == 0; + } + + @Override + public Enumeration keys() { + return Collections.enumeration(effectiveKeys); + } + + @Override + public Enumeration elements() { + final Iterator it = effectiveKeys.iterator(); + return new Enumeration() { + + @Override + public boolean hasMoreElements() { + return it.hasNext(); + } + + @Override + public Object nextElement() { + String key = it.next(); + return get(key); + } + + }; + } + + @Override + public Object get(Object key) { + try { + Attribute attr = !key.equals(LdapAttrs.objectClasses.name()) ? getAttributes().get(key.toString()) + : getAttributes().get(LdapAttrs.objectClass.name()); + if (attr == null) + return null; + Object value = attr.get(); + if (value instanceof byte[]) { + if (key.equals(LdapAttrs.userPassword.name())) + // TODO other cases (certificates, images) + return value; + value = new String((byte[]) value, StandardCharsets.UTF_8); + } + if (attr.size() == 1) + return value; + // special case for object class + if (key.equals(LdapAttrs.objectClass.name())) { + // TODO support multiple object classes + NamingEnumeration en = attr.getAll(); + String first = null; + attrs: while (en.hasMore()) { + String v = en.next().toString(); + if (v.equalsIgnoreCase(LdapObjs.top.name())) + continue attrs; + if (first == null) + first = v; + if (v.equalsIgnoreCase(getDirectory().getUserObjectClass())) + return getDirectory().getUserObjectClass(); + else if (v.equalsIgnoreCase(getDirectory().getGroupObjectClass())) + return getDirectory().getGroupObjectClass(); + } + if (first != null) + return first; + throw new IllegalStateException("Cannot find objectClass in " + value); + } else { + NamingEnumeration en = attr.getAll(); + StringJoiner values = new StringJoiner("\n"); + while (en.hasMore()) { + String v = en.next().toString(); + values.add(v); + } + return values.toString(); + } +// else +// return value; + } catch (NamingException e) { + throw new IllegalStateException("Cannot get value for attribute " + key, e); + } + } + + @Override + public Object put(String key, Object value) { + if (key == null) { + // TODO persist to other sources (like PKCS12) + char[] password = DirectoryDigestUtils.bytesToChars(value); + byte[] hashedPassword = sha1hash(password); + return put(LdapAttrs.userPassword.name(), hashedPassword); + } + if (key.startsWith("X-")) { + return put(LdapAttrs.authPassword.name(), value); + } + + getDirectory().checkEdit(); + if (!isEditing()) + startEditing(); + + if (!(value instanceof String || value instanceof byte[])) + throw new IllegalArgumentException("Value must be String or byte[]"); + + if (includeFilter && !attrFilter.contains(key)) + throw new IllegalArgumentException("Key " + key + " not included"); + else if (!includeFilter && attrFilter.contains(key)) + throw new IllegalArgumentException("Key " + key + " excluded"); + + try { + Attribute attribute = getModifiedAttributes().get(key.toString()); + // if (attribute == null) // block unit tests + attribute = new BasicAttribute(key.toString()); + if (value instanceof String && !isAsciiPrintable(((String) value))) + attribute.add(((String) value).getBytes(StandardCharsets.UTF_8)); + else + attribute.add(value); + Attribute previousAttribute = getModifiedAttributes().put(attribute); + if (previousAttribute != null) + return previousAttribute.get(); + else + return null; + } catch (NamingException e) { + throw new IllegalStateException("Cannot get value for attribute " + key, e); + } + } + + @Override + public Object remove(Object key) { + getDirectory().checkEdit(); + if (!isEditing()) + startEditing(); + + if (includeFilter && !attrFilter.contains(key)) + throw new IllegalArgumentException("Key " + key + " not included"); + else if (!includeFilter && attrFilter.contains(key)) + throw new IllegalArgumentException("Key " + key + " excluded"); + + try { + Attribute attr = getModifiedAttributes().remove(key.toString()); + if (attr != null) + return attr.get(); + else + return null; + } catch (NamingException e) { + throw new IllegalStateException("Cannot remove attribute " + key, e); + } + } + + } + +} diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDao.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDao.java index a2d9e7fc3..8e26cb44f 100644 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDao.java +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDao.java @@ -35,7 +35,7 @@ public class LdapDao extends AbstractLdapDirectoryDao { @Override public void init() { - ldapConnection = new LdapConnection(getDirectory().getUri().toString(), getDirectory().getProperties()); + ldapConnection = new LdapConnection(getDirectory().getUri().toString(), getDirectory().cloneConfigProperties()); } public void destroy() { @@ -66,26 +66,34 @@ public class LdapDao extends AbstractLdapDirectoryDao { // } @Override - public Boolean daoHasEntry(LdapName dn) { + public Boolean entryExists(LdapName dn) { try { - return daoGetEntry(dn) != null; + return doGetEntry(dn) != null; } catch (NameNotFoundException e) { return false; } } @Override - public LdapEntry daoGetEntry(LdapName name) throws NameNotFoundException { + public LdapEntry doGetEntry(LdapName name) throws NameNotFoundException { try { Attributes attrs = ldapConnection.getAttributes(name); if (attrs.size() == 0) return null; // int roleType = roleType(name); LdapEntry res; - if (isGroup(name)) + Rdn technicalRdn = LdapNameUtils.getParentRdn(name); + if (getDirectory().getGroupBaseRdn().equals(technicalRdn) + || getDirectory().getSystemRoleBaseRdn().equals(technicalRdn)) res = newGroup(name, attrs); - else + else if (getDirectory().getUserBaseRdn().equals(technicalRdn)) res = newUser(name, attrs); + else + res = new DefaultLdapEntry(getDirectory(), name, attrs); +// if (isGroup(name)) +// res = newGroup(name, attrs); +// else +// res = newUser(name, attrs); // else // throw new IllegalArgumentException("Unsupported LDAP type for " + name); return res; @@ -96,17 +104,17 @@ public class LdapDao extends AbstractLdapDirectoryDao { } } - protected boolean isGroup(LdapName dn) { - Rdn technicalRdn = LdapNameUtils.getParentRdn(dn); - if (getDirectory().getGroupBaseRdn().equals(technicalRdn) - || getDirectory().getSystemRoleBaseRdn().equals(technicalRdn)) - return true; - else if (getDirectory().getUserBaseRdn().equals(technicalRdn)) - return false; - else - throw new IllegalArgumentException( - "Cannot dind role type, " + technicalRdn + " is not a technical RDN for " + dn); - } +// protected boolean isGroup(LdapName dn) { +// Rdn technicalRdn = LdapNameUtils.getParentRdn(dn); +// if (getDirectory().getGroupBaseRdn().equals(technicalRdn) +// || getDirectory().getSystemRoleBaseRdn().equals(technicalRdn)) +// return true; +// else if (getDirectory().getUserBaseRdn().equals(technicalRdn)) +// return false; +// else +// throw new IllegalArgumentException( +// "Cannot find role type, " + technicalRdn + " is not a technical RDN for " + dn); +// } @Override public List doGetEntries(LdapName searchBase, String f, boolean deep) { @@ -237,6 +245,8 @@ public class LdapDao extends AbstractLdapDirectoryDao { @Override public HierarchyUnit doGetHierarchyUnit(LdapName dn) { try { + if (getDirectory().getBaseDn().equals(dn)) + return null; Attributes attrs = ldapConnection.getAttributes(dn); return new LdapHierarchyUnit(getDirectory(), dn, attrs); } catch (NamingException e) { diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDirectoryDao.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDirectoryDao.java index 3a0f4e6e0..273993276 100644 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDirectoryDao.java +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapDirectoryDao.java @@ -10,9 +10,9 @@ import org.argeo.util.directory.HierarchyUnit; import org.argeo.util.transaction.WorkingCopyProcessor; public interface LdapDirectoryDao extends WorkingCopyProcessor { - Boolean daoHasEntry(LdapName dn); + Boolean entryExists(LdapName dn); - LdapEntry daoGetEntry(LdapName name) throws NameNotFoundException; + LdapEntry doGetEntry(LdapName name) throws NameNotFoundException; List doGetEntries(LdapName searchBase, String filter, boolean deep); @@ -25,8 +25,8 @@ public interface LdapDirectoryDao extends WorkingCopyProcessor getReferences(String attributeId); + + public Dictionary getProperties(); + + public boolean hasCredential(String key, Object value) ; + } diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapHierarchyUnit.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapHierarchyUnit.java index 5cfca3192..a9043cc38 100644 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/LdapHierarchyUnit.java +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/LdapHierarchyUnit.java @@ -7,7 +7,7 @@ import javax.naming.ldap.Rdn; import org.argeo.util.directory.HierarchyUnit; /** LDIF/LDAP based implementation of {@link HierarchyUnit}. */ -public class LdapHierarchyUnit extends AbstractLdapEntry implements HierarchyUnit { +public class LdapHierarchyUnit extends DefaultLdapEntry implements HierarchyUnit { private final boolean functional; public LdapHierarchyUnit(AbstractLdapDirectory directory, LdapName dn, Attributes attributes) { diff --git a/org.argeo.util/src/org/argeo/util/directory/ldap/LdifDao.java b/org.argeo.util/src/org/argeo/util/directory/ldap/LdifDao.java index c805d1272..5826d86ac 100644 --- a/org.argeo.util/src/org/argeo/util/directory/ldap/LdifDao.java +++ b/org.argeo.util/src/org/argeo/util/directory/ldap/LdifDao.java @@ -217,7 +217,7 @@ public class LdifDao extends AbstractLdapDirectoryDao { */ @Override - public LdapEntry daoGetEntry(LdapName key) throws NameNotFoundException { + public LdapEntry doGetEntry(LdapName key) throws NameNotFoundException { // if (groups.containsKey(key)) // return groups.get(key); // if (users.containsKey(key)) @@ -228,7 +228,7 @@ public class LdifDao extends AbstractLdapDirectoryDao { } @Override - public Boolean daoHasEntry(LdapName dn) { + public Boolean entryExists(LdapName dn) { return entries.containsKey(dn);// || groups.containsKey(dn); } @@ -281,7 +281,7 @@ public class LdifDao extends AbstractLdapDirectoryDao { entries: for (LdapName name : entries.keySet()) { LdapEntry group; try { - LdapEntry entry = daoGetEntry(name); + LdapEntry entry = doGetEntry(name); if (AbstractLdapDirectory.hasObjectClass(entry.getAttributes(), getDirectory().getGroupObjectClass())) { group = entry; } else { @@ -329,7 +329,7 @@ public class LdifDao extends AbstractLdapDirectoryDao { Attributes modifiedAttrs = wc.getModifiedData().get(dn); LdapEntry user; try { - user = daoGetEntry(dn); + user = doGetEntry(dn); } catch (NameNotFoundException e) { throw new IllegalStateException("User to modify no found " + dn, e); } -- 2.30.2