From: Mathieu Baudier Date: Wed, 22 Jun 2022 04:27:01 +0000 (+0200) Subject: Refactor naming packages X-Git-Tag: v2.3.10~176 X-Git-Url: http://git.argeo.org/?a=commitdiff_plain;h=f520af45449b203a879392e8a0aeda6703abadfa;p=lgpl%2Fargeo-commons.git Refactor naming packages --- 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 eeec4fcfc..5ea79662a 100644 --- a/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/acr/ContentUtils.java @@ -9,6 +9,10 @@ import java.util.function.BiConsumer; import javax.xml.namespace.QName; import org.argeo.api.acr.Content; +import org.argeo.api.acr.ContentSession; +import org.argeo.cms.CmsUserManager; +import org.argeo.osgi.useradmin.UserDirectory; +import org.osgi.service.useradmin.Role; /** Utilities and routines around {@link Content}. */ public class ContentUtils { @@ -114,4 +118,12 @@ public class ContentUtils { } + public static Content roleToContent(CmsUserManager userManager, ContentSession contentSession, Role role) { + UserDirectory userDirectory = userManager.getDirectory(role); + String path = CmsContentRepository.DIRECTORY_BASE + SLASH + userDirectory.getName() + SLASH + + userDirectory.getRolePath(role); + Content content = contentSession.get(path); + return content; + } + } diff --git a/org.argeo.cms/src/org/argeo/cms/internal/osgi/DeployConfig.java b/org.argeo.cms/src/org/argeo/cms/internal/osgi/DeployConfig.java index e45b9508d..71c349327 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/osgi/DeployConfig.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/osgi/DeployConfig.java @@ -27,9 +27,9 @@ import org.argeo.cms.internal.runtime.InitUtils; import org.argeo.cms.internal.runtime.KernelConstants; import org.argeo.cms.internal.runtime.KernelUtils; import org.argeo.osgi.useradmin.UserAdminConf; -import org.argeo.util.naming.AttributesDictionary; -import org.argeo.util.naming.LdifParser; -import org.argeo.util.naming.LdifWriter; +import org.argeo.util.naming.ldap.AttributesDictionary; +import org.argeo.util.naming.ldap.LdifParser; +import org.argeo.util.naming.ldap.LdifWriter; import org.osgi.framework.InvalidSyntaxException; import org.osgi.service.cm.Configuration; import org.osgi.service.cm.ConfigurationAdmin; diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java index 890e28391..10cb98dd3 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsUserAdmin.java @@ -41,7 +41,7 @@ import org.argeo.osgi.useradmin.LdifUserAdmin; import org.argeo.osgi.useradmin.OsUserDirectory; import org.argeo.osgi.useradmin.UserAdminConf; import org.argeo.osgi.useradmin.UserDirectory; -import org.argeo.util.naming.DnsBrowser; +import org.argeo.util.naming.dns.DnsBrowser; import org.ietf.jgss.GSSCredential; import org.ietf.jgss.GSSException; import org.ietf.jgss.GSSManager; diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/IpaUtils.java b/org.argeo.util/src/org/argeo/osgi/useradmin/IpaUtils.java index 780215a9c..e1c8136f5 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/IpaUtils.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/IpaUtils.java @@ -13,8 +13,8 @@ import javax.naming.InvalidNameException; import javax.naming.NamingException; import javax.naming.ldap.LdapName; -import org.argeo.util.naming.DnsBrowser; import org.argeo.util.naming.LdapAttrs; +import org.argeo.util.naming.dns.DnsBrowser; /** Free IPA specific conventions. */ public class IpaUtils { 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 db83b81e4..a20ad85c1 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java @@ -21,10 +21,10 @@ import javax.naming.directory.Attributes; import javax.naming.directory.BasicAttribute; import javax.naming.ldap.LdapName; -import org.argeo.util.naming.AuthPassword; import org.argeo.util.naming.LdapAttrs; import org.argeo.util.naming.LdapObjs; import org.argeo.util.naming.SharedSecret; +import org.argeo.util.naming.ldap.AuthPassword; /** Directory user implementation */ abstract class LdifUser implements DirectoryUser { 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 ef487594c..bf455949d 100644 --- a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUserAdmin.java +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUserAdmin.java @@ -29,8 +29,8 @@ import javax.naming.directory.Attributes; import javax.naming.ldap.LdapName; import org.argeo.util.naming.LdapObjs; -import org.argeo.util.naming.LdifParser; -import org.argeo.util.naming.LdifWriter; +import org.argeo.util.naming.ldap.LdifParser; +import org.argeo.util.naming.ldap.LdifWriter; import org.osgi.framework.Filter; import org.osgi.service.useradmin.Role; import org.osgi.service.useradmin.User; diff --git a/org.argeo.util/src/org/argeo/util/naming/AttributesDictionary.java b/org.argeo.util/src/org/argeo/util/naming/AttributesDictionary.java deleted file mode 100644 index 7c645f3a3..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/AttributesDictionary.java +++ /dev/null @@ -1,171 +0,0 @@ -package org.argeo.util.naming; - -import java.util.Dictionary; -import java.util.Enumeration; - -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.BasicAttribute; - -public class AttributesDictionary extends Dictionary { - private final Attributes attributes; - - /** The provided attributes is wrapped, not copied. */ - public AttributesDictionary(Attributes attributes) { - if (attributes == null) - throw new IllegalArgumentException("Attributes cannot be null"); - this.attributes = attributes; - } - - @Override - public int size() { - return attributes.size(); - } - - @Override - public boolean isEmpty() { - return attributes.size() == 0; - } - - @Override - public Enumeration keys() { - NamingEnumeration namingEnumeration = attributes.getIDs(); - return new Enumeration() { - - @Override - public boolean hasMoreElements() { - return namingEnumeration.hasMoreElements(); - } - - @Override - public String nextElement() { - return namingEnumeration.nextElement(); - } - - }; - } - - @Override - public Enumeration elements() { - NamingEnumeration namingEnumeration = attributes.getIDs(); - return new Enumeration() { - - @Override - public boolean hasMoreElements() { - return namingEnumeration.hasMoreElements(); - } - - @Override - public Object nextElement() { - String key = namingEnumeration.nextElement(); - return get(key); - } - - }; - } - - @Override - /** @returns a String or String[] */ - public Object get(Object key) { - try { - if (key == null) - throw new IllegalArgumentException("Key cannot be null"); - Attribute attr = attributes.get(key.toString()); - if (attr == null) - return null; - if (attr.size() == 0) - throw new IllegalStateException("There must be at least one value"); - else if (attr.size() == 1) { - return attr.get().toString(); - } else {// multiple - String[] res = new String[attr.size()]; - for (int i = 0; i < attr.size(); i++) { - Object value = attr.get(); - if (value == null) - throw new RuntimeException("Values cannot be null"); - res[i] = attr.get(i).toString(); - } - return res; - } - } catch (NamingException e) { - throw new RuntimeException("Cannot get value for " + key, e); - } - } - - @Override - public Object put(String key, Object value) { - if (key == null) - throw new IllegalArgumentException("Key cannot be null"); - if (value == null) - throw new IllegalArgumentException("Value cannot be null"); - - Object oldValue = get(key); - Attribute attr = attributes.get(key); - if (attr == null) { - attr = new BasicAttribute(key); - attributes.put(attr); - } - - if (value instanceof String[]) { - String[] values = (String[]) value; - // clean additional values - for (int i = values.length; i < attr.size(); i++) - attr.remove(i); - // set values - for (int i = 0; i < values.length; i++) { - attr.set(i, values[i]); - } - } else { - if (attr.size() > 1) - throw new IllegalArgumentException("Attribute " + key + " is multi-valued"); - if (attr.size() == 1) { - try { - if (!attr.get(0).equals(value)) - attr.set(0, value.toString()); - } catch (NamingException e) { - throw new RuntimeException("Cannot check existing value", e); - } - } else { - attr.add(value.toString()); - } - } - return oldValue; - } - - @Override - public Object remove(Object key) { - if (key == null) - throw new IllegalArgumentException("Key cannot be null"); - Object oldValue = get(key); - if (oldValue == null) - return null; - return attributes.remove(key.toString()); - } - - /** - * Copy the content of an {@link Attributes} to the provided - * {@link Dictionary}. - */ - public static void copy(Attributes attributes, Dictionary dictionary) { - AttributesDictionary ad = new AttributesDictionary(attributes); - Enumeration keys = ad.keys(); - while (keys.hasMoreElements()) { - String key = keys.nextElement(); - dictionary.put(key, ad.get(key)); - } - } - - /** - * Copy a {@link Dictionary} into an {@link Attributes}. - */ - public static void copy(Dictionary dictionary, Attributes attributes) { - AttributesDictionary ad = new AttributesDictionary(attributes); - Enumeration keys = dictionary.keys(); - while (keys.hasMoreElements()) { - String key = keys.nextElement(); - ad.put(key, dictionary.get(key)); - } - } -} diff --git a/org.argeo.util/src/org/argeo/util/naming/AuthPassword.java b/org.argeo.util/src/org/argeo/util/naming/AuthPassword.java deleted file mode 100644 index d8792729f..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/AuthPassword.java +++ /dev/null @@ -1,138 +0,0 @@ -package org.argeo.util.naming; - -import java.io.IOException; -import java.util.Arrays; -import java.util.StringTokenizer; - -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.security.auth.callback.Callback; -import javax.security.auth.callback.CallbackHandler; -import javax.security.auth.callback.NameCallback; -import javax.security.auth.callback.PasswordCallback; -import javax.security.auth.callback.UnsupportedCallbackException; - -/** LDAP authPassword field according to RFC 3112 */ -public class AuthPassword implements CallbackHandler { - private final String authScheme; - private final String authInfo; - private final String authValue; - - public AuthPassword(String value) { - StringTokenizer st = new StringTokenizer(value, "$"); - // TODO make it more robust, deal with bad formatting - this.authScheme = st.nextToken().trim(); - this.authInfo = st.nextToken().trim(); - this.authValue = st.nextToken().trim(); - - String expectedAuthScheme = getExpectedAuthScheme(); - if (expectedAuthScheme != null && !authScheme.equals(expectedAuthScheme)) - throw new IllegalArgumentException( - "Auth scheme " + authScheme + " is not compatible with " + expectedAuthScheme); - } - - protected AuthPassword(String authInfo, String authValue) { - this.authScheme = getExpectedAuthScheme(); - if (authScheme == null) - throw new IllegalArgumentException("Expected auth scheme cannot be null"); - this.authInfo = authInfo; - this.authValue = authValue; - } - - protected AuthPassword(AuthPassword authPassword) { - this.authScheme = authPassword.getAuthScheme(); - this.authInfo = authPassword.getAuthInfo(); - this.authValue = authPassword.getAuthValue(); - } - - protected String getExpectedAuthScheme() { - return null; - } - - protected boolean matchAuthValue(Object object) { - return authValue.equals(object.toString()); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof AuthPassword)) - return false; - AuthPassword authPassword = (AuthPassword) obj; - return authScheme.equals(authPassword.authScheme) && authInfo.equals(authPassword.authInfo) - && authValue.equals(authValue); - } - - public boolean keyEquals(AuthPassword authPassword) { - return authScheme.equals(authPassword.authScheme) && authInfo.equals(authPassword.authInfo); - } - - @Override - public int hashCode() { - return authValue.hashCode(); - } - - @Override - public String toString() { - return toAuthPassword(); - } - - public final String toAuthPassword() { - return getAuthScheme() + '$' + authInfo + '$' + authValue; - } - - public String getAuthScheme() { - return authScheme; - } - - public String getAuthInfo() { - return authInfo; - } - - public String getAuthValue() { - return authValue; - } - - public static AuthPassword matchAuthValue(Attributes attributes, char[] value) { - try { - Attribute authPassword = attributes.get(LdapAttrs.authPassword.name()); - if (authPassword != null) { - NamingEnumeration values = authPassword.getAll(); - while (values.hasMore()) { - Object val = values.next(); - AuthPassword token = new AuthPassword(val.toString()); - String auth; - if (Arrays.binarySearch(value, '$') >= 0) { - auth = token.authInfo + '$' + token.authValue; - } else { - auth = token.authValue; - } - if (Arrays.equals(auth.toCharArray(), value)) - return token; - // if (token.matchAuthValue(value)) - // return token; - } - } - return null; - } catch (NamingException e) { - throw new IllegalStateException("Cannot check attribute", e); - } - } - - public static boolean remove(Attributes attributes, AuthPassword value) { - Attribute authPassword = attributes.get(LdapAttrs.authPassword.name()); - return authPassword.remove(value.toAuthPassword()); - } - - @Override - public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { - for (Callback callback : callbacks) { - if (callback instanceof NameCallback) - ((NameCallback) callback).setName(toAuthPassword()); - else if (callback instanceof PasswordCallback) - ((PasswordCallback) callback).setPassword(getAuthValue().toCharArray()); - } - } - -} diff --git a/org.argeo.util/src/org/argeo/util/naming/DnsBrowser.java b/org.argeo.util/src/org/argeo/util/naming/DnsBrowser.java deleted file mode 100644 index 1a67eea36..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/DnsBrowser.java +++ /dev/null @@ -1,179 +0,0 @@ -package org.argeo.util.naming; - -import java.io.Closeable; -import java.io.IOException; -import java.io.PrintStream; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Collections; -import java.util.Hashtable; -import java.util.List; -import java.util.Map; -import java.util.SortedSet; -import java.util.TreeMap; -import java.util.TreeSet; - -import javax.naming.Binding; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.DirContext; -import javax.naming.directory.InitialDirContext; - -public class DnsBrowser implements Closeable { - private final DirContext initialCtx; - - public DnsBrowser() throws NamingException { - this(null); - } - - public DnsBrowser(String dnsServerUrls) throws NamingException { - Hashtable env = new Hashtable<>(); - env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); - if (dnsServerUrls != null) - env.put("java.naming.provider.url", dnsServerUrls); - initialCtx = new InitialDirContext(env); - } - - public Map> getAllRecords(String name) throws NamingException { - Map> res = new TreeMap<>(); - Attributes attrs = initialCtx.getAttributes(name); - NamingEnumeration ids = attrs.getIDs(); - while (ids.hasMore()) { - String recordType = ids.next(); - List lst = new ArrayList(); - res.put(recordType, lst); - Attribute attr = attrs.get(recordType); - addValues(attr, lst); - } - return Collections.unmodifiableMap(res); - } - - /** - * Return a single record (typically A, AAAA, etc. or null if not available. - * Will fail if multiple records. - */ - public String getRecord(String name, String recordType) throws NamingException { - Attributes attrs = initialCtx.getAttributes(name, new String[] { recordType }); - if (attrs.size() == 0) - return null; - Attribute attr = attrs.get(recordType); - if (attr.size() > 1) - throw new IllegalArgumentException("Multiple record type " + recordType); - assert attr.size() != 0; - Object value = attr.get(); - assert value != null; - return value.toString(); - } - - /** - * Return records of a given type. - */ - public List getRecords(String name, String recordType) throws NamingException { - List res = new ArrayList(); - Attributes attrs = initialCtx.getAttributes(name, new String[] { recordType }); - Attribute attr = attrs.get(recordType); - addValues(attr, res); - return res; - } - - /** Ordered, with preferred first. */ - public List getSrvRecordsAsHosts(String name, boolean withPort) throws NamingException { - List raw = getRecords(name, "SRV"); - if (raw.size() == 0) - return null; - SortedSet res = new TreeSet<>(); - for (int i = 0; i < raw.size(); i++) { - String record = raw.get(i); - String[] arr = record.split(" "); - Integer priority = Integer.parseInt(arr[0]); - Integer weight = Integer.parseInt(arr[1]); - Integer port = Integer.parseInt(arr[2]); - String hostname = arr[3]; - SrvRecord order = new SrvRecord(priority, weight, port, hostname); - res.add(order); - } - List lst = new ArrayList<>(); - for (SrvRecord order : res) { - lst.add(order.toHost(withPort)); - } - return Collections.unmodifiableList(lst); - } - - private void addValues(Attribute attr, List lst) throws NamingException { - NamingEnumeration values = attr.getAll(); - while (values.hasMore()) { - Object value = values.next(); - if (value != null) { - if (value instanceof byte[]) { - String str = Base64.getEncoder().encodeToString((byte[]) value); - lst.add(str); - } else - lst.add(value.toString()); - } - } - - } - - public List listEntries(String name) throws NamingException { - List res = new ArrayList(); - NamingEnumeration ne = initialCtx.listBindings(name); - while (ne.hasMore()) { - Binding b = ne.next(); - res.add(b.getName()); - } - return Collections.unmodifiableList(res); - } - - @Override - public void close() throws IOException { - destroy(); - } - - public void destroy() { - try { - initialCtx.close(); - } catch (NamingException e) { - // silent - } - } - - public static void main(String[] args) { - if (args.length == 0) { - printUsage(System.err); - System.exit(1); - } - try (DnsBrowser dnsBrowser = new DnsBrowser()) { - String hostname = args[0]; - String recordType = args.length > 1 ? args[1] : "A"; - if (recordType.equals("*")) { - Map> records = dnsBrowser.getAllRecords(hostname); - for (String type : records.keySet()) { - for (String record : records.get(type)) { - String typeLabel; - if ("44".equals(type)) - typeLabel = "SSHFP"; - else if ("46".equals(type)) - typeLabel = "RRSIG"; - else if ("48".equals(type)) - typeLabel = "DNSKEY"; - else - typeLabel = type; - System.out.println(typeLabel + "\t" + record); - } - } - } else { - System.out.println(dnsBrowser.getRecord(hostname, recordType)); - } - - } catch (Exception e) { - e.printStackTrace(); - } - } - - public static void printUsage(PrintStream out) { - out.println("java org.argeo.naming.DnsBrowser [ | *]"); - } - -} \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/util/naming/LdifParser.java b/org.argeo.util/src/org/argeo/util/naming/LdifParser.java deleted file mode 100644 index 69ceb129c..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/LdifParser.java +++ /dev/null @@ -1,159 +0,0 @@ -package org.argeo.util.naming; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Base64; -import java.util.List; -import java.util.SortedMap; -import java.util.TreeMap; - -import javax.naming.InvalidNameException; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.BasicAttribute; -import javax.naming.directory.BasicAttributes; -import javax.naming.ldap.LdapName; -import javax.naming.ldap.Rdn; - -/** Basic LDIF parser. */ -public class LdifParser { - private final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - - protected Attributes addAttributes(SortedMap res, int lineNumber, LdapName currentDn, - Attributes currentAttributes) { - try { - Rdn nameRdn = currentDn.getRdn(currentDn.size() - 1); - Attribute nameAttr = currentAttributes.get(nameRdn.getType()); - if (nameAttr == null) - currentAttributes.put(nameRdn.getType(), nameRdn.getValue()); - else if (!nameAttr.get().equals(nameRdn.getValue())) - throw new IllegalStateException( - "Attribute " + nameAttr.getID() + "=" + nameAttr.get() + " not consistent with DN " + currentDn - + " (shortly before line " + lineNumber + " in LDIF file)"); - Attributes previous = res.put(currentDn, currentAttributes); - return previous; - } catch (NamingException e) { - throw new IllegalStateException("Cannot add " + currentDn, e); - } - } - - /** With UTF-8 charset */ - public SortedMap read(InputStream in) throws IOException { - try (Reader reader = new InputStreamReader(in, DEFAULT_CHARSET)) { - return read(reader); - } finally { - try { - in.close(); - } catch (IOException e) { - // silent - } - } - } - - /** Will close the reader. */ - public SortedMap read(Reader reader) throws IOException { - SortedMap res = new TreeMap(); - try { - List lines = new ArrayList<>(); - try (BufferedReader br = new BufferedReader(reader)) { - String line; - while ((line = br.readLine()) != null) { - lines.add(line); - } - } - if (lines.size() == 0) - return res; - // add an empty new line since the last line is not checked - if (!lines.get(lines.size() - 1).equals("")) - lines.add(""); - - LdapName currentDn = null; - Attributes currentAttributes = null; - StringBuilder currentEntry = new StringBuilder(); - - readLines: for (int lineNumber = 0; lineNumber < lines.size(); lineNumber++) { - String line = lines.get(lineNumber); - boolean isLastLine = false; - if (lineNumber == lines.size() - 1) - isLastLine = true; - if (line.startsWith(" ")) { - currentEntry.append(line.substring(1)); - if (!isLastLine) - continue readLines; - } - - if (currentEntry.length() != 0 || isLastLine) { - // read previous attribute - StringBuilder attrId = new StringBuilder(8); - boolean isBase64 = false; - readAttrId: for (int i = 0; i < currentEntry.length(); i++) { - char c = currentEntry.charAt(i); - if (c == ':') { - if (i + 1 < currentEntry.length() && currentEntry.charAt(i + 1) == ':') - isBase64 = true; - currentEntry.delete(0, i + (isBase64 ? 2 : 1)); - break readAttrId; - } else { - attrId.append(c); - } - } - - String attributeId = attrId.toString(); - // TODO should we really trim the end of the string as well? - String cleanValueStr = currentEntry.toString().trim(); - Object attributeValue = isBase64 ? Base64.getDecoder().decode(cleanValueStr) : cleanValueStr; - - // manage DN attributes - if (attributeId.equals(LdapAttrs.DN) || isLastLine) { - if (currentDn != null) { - // - // ADD - // - Attributes previous = addAttributes(res, lineNumber, currentDn, currentAttributes); - if (previous != null) { -// log.warn("There was already an entry with DN " + currentDn -// + ", which has been discarded by a subsequent one."); - } - } - - if (attributeId.equals(LdapAttrs.DN)) - try { - currentDn = new LdapName(attributeValue.toString()); - currentAttributes = new BasicAttributes(true); - } catch (InvalidNameException e) { -// log.error(attributeValue + " not a valid DN, skipping the entry."); - currentDn = null; - currentAttributes = null; - } - } - - // store attribute - if (currentAttributes != null) { - Attribute attribute = currentAttributes.get(attributeId); - if (attribute == null) { - attribute = new BasicAttribute(attributeId); - currentAttributes.put(attribute); - } - attribute.add(attributeValue); - } - currentEntry = new StringBuilder(); - } - currentEntry.append(line); - } - } finally { - try { - reader.close(); - } catch (IOException e) { - // silent - } - } - return res; - } -} \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/util/naming/LdifWriter.java b/org.argeo.util/src/org/argeo/util/naming/LdifWriter.java deleted file mode 100644 index 496e53b1b..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/LdifWriter.java +++ /dev/null @@ -1,104 +0,0 @@ -package org.argeo.util.naming; - -import static org.argeo.util.naming.LdapAttrs.DN; -import static org.argeo.util.naming.LdapAttrs.member; -import static org.argeo.util.naming.LdapAttrs.objectClass; -import static org.argeo.util.naming.LdapAttrs.uniqueMember; - -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.Map; -import java.util.SortedSet; -import java.util.TreeSet; - -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.ldap.LdapName; -import javax.naming.ldap.Rdn; - -/** Basic LDIF writer */ -public class LdifWriter { - private final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - private final Writer writer; - - /** Writer must be closed by caller */ - public LdifWriter(Writer writer) { - this.writer = writer; - } - - /** Stream must be closed by caller */ - public LdifWriter(OutputStream out) { - this(new OutputStreamWriter(out, DEFAULT_CHARSET)); - } - - public void writeEntry(LdapName name, Attributes attributes) throws IOException { - try { - // check consistency - Rdn nameRdn = name.getRdn(name.size() - 1); - Attribute nameAttr = attributes.get(nameRdn.getType()); - if (!nameAttr.get().equals(nameRdn.getValue())) - throw new IllegalArgumentException( - "Attribute " + nameAttr.getID() + "=" + nameAttr.get() + " not consistent with DN " + name); - - writer.append(DN + ": ").append(name.toString()).append('\n'); - Attribute objectClassAttr = attributes.get(objectClass.name()); - if (objectClassAttr != null) - writeAttribute(objectClassAttr); - attributes: for (NamingEnumeration attrs = attributes.getAll(); attrs.hasMore();) { - Attribute attribute = attrs.next(); - if (attribute.getID().equals(DN) || attribute.getID().equals(objectClass.name())) - continue attributes;// skip DN attribute - if (attribute.getID().equals(member.name()) || attribute.getID().equals(uniqueMember.name())) - continue attributes;// skip member and uniqueMember attributes, so that they are always written last - writeAttribute(attribute); - } - // write member and uniqueMember attributes last - for (NamingEnumeration attrs = attributes.getAll(); attrs.hasMore();) { - Attribute attribute = attrs.next(); - if (attribute.getID().equals(member.name()) || attribute.getID().equals(uniqueMember.name())) - writeMemberAttribute(attribute); - } - writer.append('\n'); - writer.flush(); - } catch (NamingException e) { - throw new IllegalStateException("Cannot write LDIF", e); - } - } - - public void write(Map entries) throws IOException { - for (LdapName dn : entries.keySet()) - writeEntry(dn, entries.get(dn)); - } - - protected void writeAttribute(Attribute attribute) throws NamingException, IOException { - for (NamingEnumeration attrValues = attribute.getAll(); attrValues.hasMore();) { - Object value = attrValues.next(); - if (value instanceof byte[]) { - String encoded = Base64.getEncoder().encodeToString((byte[]) value); - writer.append(attribute.getID()).append(":: ").append(encoded).append('\n'); - } else { - writer.append(attribute.getID()).append(": ").append(value.toString()).append('\n'); - } - } - } - - protected void writeMemberAttribute(Attribute attribute) throws NamingException, IOException { - // Note: duplicate entries will be swallowed - SortedSet values = new TreeSet<>(); - for (NamingEnumeration attrValues = attribute.getAll(); attrValues.hasMore();) { - String value = attrValues.next().toString(); - values.add(value); - } - - for (String value : values) { - writer.append(attribute.getID()).append(": ").append(value).append('\n'); - } - } -} diff --git a/org.argeo.util/src/org/argeo/util/naming/SharedSecret.java b/org.argeo.util/src/org/argeo/util/naming/SharedSecret.java index 7f0575407..e38bc2f29 100644 --- a/org.argeo.util/src/org/argeo/util/naming/SharedSecret.java +++ b/org.argeo.util/src/org/argeo/util/naming/SharedSecret.java @@ -4,6 +4,8 @@ import java.time.Instant; import java.time.ZoneOffset; import java.time.ZonedDateTime; +import org.argeo.util.naming.ldap.AuthPassword; + public class SharedSecret extends AuthPassword { public final static String X_SHARED_SECRET = "X-SharedSecret"; private final Instant expiry; diff --git a/org.argeo.util/src/org/argeo/util/naming/SrvRecord.java b/org.argeo.util/src/org/argeo/util/naming/SrvRecord.java deleted file mode 100644 index f2476a930..000000000 --- a/org.argeo.util/src/org/argeo/util/naming/SrvRecord.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.argeo.util.naming; - -class SrvRecord implements Comparable { - private final Integer priority; - private final Integer weight; - private final Integer port; - private final String hostname; - - public SrvRecord(Integer priority, Integer weight, Integer port, String hostname) { - this.priority = priority; - this.weight = weight; - this.port = port; - this.hostname = hostname; - } - - @Override - public int compareTo(SrvRecord other) { - // https: // en.wikipedia.org/wiki/SRV_record - if (priority != other.priority) - return priority - other.priority; - if (weight != other.weight) - return other.weight - other.weight; - String host = toHost(false); - String otherHost = other.toHost(false); - if (host.length() == otherHost.length()) - return host.compareTo(otherHost); - else - return host.length() - otherHost.length(); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof SrvRecord) { - SrvRecord other = (SrvRecord) obj; - return priority == other.priority && weight == other.weight && port == other.port - && hostname.equals(other.hostname); - } - return false; - } - - @Override - public String toString() { - return priority + " " + weight; - } - - public String toHost(boolean withPort) { - String hostStr = hostname; - if (hostname.charAt(hostname.length() - 1) == '.') - hostStr = hostname.substring(0, hostname.length() - 1); - return hostStr + (withPort ? ":" + port : ""); - } -} diff --git a/org.argeo.util/src/org/argeo/util/naming/dns/DnsBrowser.java b/org.argeo.util/src/org/argeo/util/naming/dns/DnsBrowser.java new file mode 100644 index 000000000..4bd05268c --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/naming/dns/DnsBrowser.java @@ -0,0 +1,179 @@ +package org.argeo.util.naming.dns; + +import java.io.Closeable; +import java.io.IOException; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Collections; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeMap; +import java.util.TreeSet; + +import javax.naming.Binding; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; + +public class DnsBrowser implements Closeable { + private final DirContext initialCtx; + + public DnsBrowser() throws NamingException { + this(null); + } + + public DnsBrowser(String dnsServerUrls) throws NamingException { + Hashtable env = new Hashtable<>(); + env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory"); + if (dnsServerUrls != null) + env.put("java.naming.provider.url", dnsServerUrls); + initialCtx = new InitialDirContext(env); + } + + public Map> getAllRecords(String name) throws NamingException { + Map> res = new TreeMap<>(); + Attributes attrs = initialCtx.getAttributes(name); + NamingEnumeration ids = attrs.getIDs(); + while (ids.hasMore()) { + String recordType = ids.next(); + List lst = new ArrayList(); + res.put(recordType, lst); + Attribute attr = attrs.get(recordType); + addValues(attr, lst); + } + return Collections.unmodifiableMap(res); + } + + /** + * Return a single record (typically A, AAAA, etc. or null if not available. + * Will fail if multiple records. + */ + public String getRecord(String name, String recordType) throws NamingException { + Attributes attrs = initialCtx.getAttributes(name, new String[] { recordType }); + if (attrs.size() == 0) + return null; + Attribute attr = attrs.get(recordType); + if (attr.size() > 1) + throw new IllegalArgumentException("Multiple record type " + recordType); + assert attr.size() != 0; + Object value = attr.get(); + assert value != null; + return value.toString(); + } + + /** + * Return records of a given type. + */ + public List getRecords(String name, String recordType) throws NamingException { + List res = new ArrayList(); + Attributes attrs = initialCtx.getAttributes(name, new String[] { recordType }); + Attribute attr = attrs.get(recordType); + addValues(attr, res); + return res; + } + + /** Ordered, with preferred first. */ + public List getSrvRecordsAsHosts(String name, boolean withPort) throws NamingException { + List raw = getRecords(name, "SRV"); + if (raw.size() == 0) + return null; + SortedSet res = new TreeSet<>(); + for (int i = 0; i < raw.size(); i++) { + String record = raw.get(i); + String[] arr = record.split(" "); + Integer priority = Integer.parseInt(arr[0]); + Integer weight = Integer.parseInt(arr[1]); + Integer port = Integer.parseInt(arr[2]); + String hostname = arr[3]; + SrvRecord order = new SrvRecord(priority, weight, port, hostname); + res.add(order); + } + List lst = new ArrayList<>(); + for (SrvRecord order : res) { + lst.add(order.toHost(withPort)); + } + return Collections.unmodifiableList(lst); + } + + private void addValues(Attribute attr, List lst) throws NamingException { + NamingEnumeration values = attr.getAll(); + while (values.hasMore()) { + Object value = values.next(); + if (value != null) { + if (value instanceof byte[]) { + String str = Base64.getEncoder().encodeToString((byte[]) value); + lst.add(str); + } else + lst.add(value.toString()); + } + } + + } + + public List listEntries(String name) throws NamingException { + List res = new ArrayList(); + NamingEnumeration ne = initialCtx.listBindings(name); + while (ne.hasMore()) { + Binding b = ne.next(); + res.add(b.getName()); + } + return Collections.unmodifiableList(res); + } + + @Override + public void close() throws IOException { + destroy(); + } + + public void destroy() { + try { + initialCtx.close(); + } catch (NamingException e) { + // silent + } + } + + public static void main(String[] args) { + if (args.length == 0) { + printUsage(System.err); + System.exit(1); + } + try (DnsBrowser dnsBrowser = new DnsBrowser()) { + String hostname = args[0]; + String recordType = args.length > 1 ? args[1] : "A"; + if (recordType.equals("*")) { + Map> records = dnsBrowser.getAllRecords(hostname); + for (String type : records.keySet()) { + for (String record : records.get(type)) { + String typeLabel; + if ("44".equals(type)) + typeLabel = "SSHFP"; + else if ("46".equals(type)) + typeLabel = "RRSIG"; + else if ("48".equals(type)) + typeLabel = "DNSKEY"; + else + typeLabel = type; + System.out.println(typeLabel + "\t" + record); + } + } + } else { + System.out.println(dnsBrowser.getRecord(hostname, recordType)); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + + public static void printUsage(PrintStream out) { + out.println("java org.argeo.naming.DnsBrowser [ | *]"); + } + +} \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/util/naming/dns/SrvRecord.java b/org.argeo.util/src/org/argeo/util/naming/dns/SrvRecord.java new file mode 100644 index 000000000..ea6f3cc96 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/naming/dns/SrvRecord.java @@ -0,0 +1,52 @@ +package org.argeo.util.naming.dns; + +class SrvRecord implements Comparable { + private final Integer priority; + private final Integer weight; + private final Integer port; + private final String hostname; + + public SrvRecord(Integer priority, Integer weight, Integer port, String hostname) { + this.priority = priority; + this.weight = weight; + this.port = port; + this.hostname = hostname; + } + + @Override + public int compareTo(SrvRecord other) { + // https: // en.wikipedia.org/wiki/SRV_record + if (priority != other.priority) + return priority - other.priority; + if (weight != other.weight) + return other.weight - other.weight; + String host = toHost(false); + String otherHost = other.toHost(false); + if (host.length() == otherHost.length()) + return host.compareTo(otherHost); + else + return host.length() - otherHost.length(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof SrvRecord) { + SrvRecord other = (SrvRecord) obj; + return priority == other.priority && weight == other.weight && port == other.port + && hostname.equals(other.hostname); + } + return false; + } + + @Override + public String toString() { + return priority + " " + weight; + } + + public String toHost(boolean withPort) { + String hostStr = hostname; + if (hostname.charAt(hostname.length() - 1) == '.') + hostStr = hostname.substring(0, hostname.length() - 1); + return hostStr + (withPort ? ":" + port : ""); + } +} diff --git a/org.argeo.util/src/org/argeo/util/naming/ldap/AttributesDictionary.java b/org.argeo.util/src/org/argeo/util/naming/ldap/AttributesDictionary.java new file mode 100644 index 000000000..0bbeb03fe --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/naming/ldap/AttributesDictionary.java @@ -0,0 +1,171 @@ +package org.argeo.util.naming.ldap; + +import java.util.Dictionary; +import java.util.Enumeration; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttribute; + +public class AttributesDictionary extends Dictionary { + private final Attributes attributes; + + /** The provided attributes is wrapped, not copied. */ + public AttributesDictionary(Attributes attributes) { + if (attributes == null) + throw new IllegalArgumentException("Attributes cannot be null"); + this.attributes = attributes; + } + + @Override + public int size() { + return attributes.size(); + } + + @Override + public boolean isEmpty() { + return attributes.size() == 0; + } + + @Override + public Enumeration keys() { + NamingEnumeration namingEnumeration = attributes.getIDs(); + return new Enumeration() { + + @Override + public boolean hasMoreElements() { + return namingEnumeration.hasMoreElements(); + } + + @Override + public String nextElement() { + return namingEnumeration.nextElement(); + } + + }; + } + + @Override + public Enumeration elements() { + NamingEnumeration namingEnumeration = attributes.getIDs(); + return new Enumeration() { + + @Override + public boolean hasMoreElements() { + return namingEnumeration.hasMoreElements(); + } + + @Override + public Object nextElement() { + String key = namingEnumeration.nextElement(); + return get(key); + } + + }; + } + + @Override + /** @returns a String or String[] */ + public Object get(Object key) { + try { + if (key == null) + throw new IllegalArgumentException("Key cannot be null"); + Attribute attr = attributes.get(key.toString()); + if (attr == null) + return null; + if (attr.size() == 0) + throw new IllegalStateException("There must be at least one value"); + else if (attr.size() == 1) { + return attr.get().toString(); + } else {// multiple + String[] res = new String[attr.size()]; + for (int i = 0; i < attr.size(); i++) { + Object value = attr.get(); + if (value == null) + throw new RuntimeException("Values cannot be null"); + res[i] = attr.get(i).toString(); + } + return res; + } + } catch (NamingException e) { + throw new RuntimeException("Cannot get value for " + key, e); + } + } + + @Override + public Object put(String key, Object value) { + if (key == null) + throw new IllegalArgumentException("Key cannot be null"); + if (value == null) + throw new IllegalArgumentException("Value cannot be null"); + + Object oldValue = get(key); + Attribute attr = attributes.get(key); + if (attr == null) { + attr = new BasicAttribute(key); + attributes.put(attr); + } + + if (value instanceof String[]) { + String[] values = (String[]) value; + // clean additional values + for (int i = values.length; i < attr.size(); i++) + attr.remove(i); + // set values + for (int i = 0; i < values.length; i++) { + attr.set(i, values[i]); + } + } else { + if (attr.size() > 1) + throw new IllegalArgumentException("Attribute " + key + " is multi-valued"); + if (attr.size() == 1) { + try { + if (!attr.get(0).equals(value)) + attr.set(0, value.toString()); + } catch (NamingException e) { + throw new RuntimeException("Cannot check existing value", e); + } + } else { + attr.add(value.toString()); + } + } + return oldValue; + } + + @Override + public Object remove(Object key) { + if (key == null) + throw new IllegalArgumentException("Key cannot be null"); + Object oldValue = get(key); + if (oldValue == null) + return null; + return attributes.remove(key.toString()); + } + + /** + * Copy the content of an {@link Attributes} to the provided + * {@link Dictionary}. + */ + public static void copy(Attributes attributes, Dictionary dictionary) { + AttributesDictionary ad = new AttributesDictionary(attributes); + Enumeration keys = ad.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + dictionary.put(key, ad.get(key)); + } + } + + /** + * Copy a {@link Dictionary} into an {@link Attributes}. + */ + public static void copy(Dictionary dictionary, Attributes attributes) { + AttributesDictionary ad = new AttributesDictionary(attributes); + Enumeration keys = dictionary.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + ad.put(key, dictionary.get(key)); + } + } +} diff --git a/org.argeo.util/src/org/argeo/util/naming/ldap/AuthPassword.java b/org.argeo.util/src/org/argeo/util/naming/ldap/AuthPassword.java new file mode 100644 index 000000000..b11684020 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/naming/ldap/AuthPassword.java @@ -0,0 +1,140 @@ +package org.argeo.util.naming.ldap; + +import java.io.IOException; +import java.util.Arrays; +import java.util.StringTokenizer; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; + +import org.argeo.util.naming.LdapAttrs; + +/** LDAP authPassword field according to RFC 3112 */ +public class AuthPassword implements CallbackHandler { + private final String authScheme; + private final String authInfo; + private final String authValue; + + public AuthPassword(String value) { + StringTokenizer st = new StringTokenizer(value, "$"); + // TODO make it more robust, deal with bad formatting + this.authScheme = st.nextToken().trim(); + this.authInfo = st.nextToken().trim(); + this.authValue = st.nextToken().trim(); + + String expectedAuthScheme = getExpectedAuthScheme(); + if (expectedAuthScheme != null && !authScheme.equals(expectedAuthScheme)) + throw new IllegalArgumentException( + "Auth scheme " + authScheme + " is not compatible with " + expectedAuthScheme); + } + + protected AuthPassword(String authInfo, String authValue) { + this.authScheme = getExpectedAuthScheme(); + if (authScheme == null) + throw new IllegalArgumentException("Expected auth scheme cannot be null"); + this.authInfo = authInfo; + this.authValue = authValue; + } + + protected AuthPassword(AuthPassword authPassword) { + this.authScheme = authPassword.getAuthScheme(); + this.authInfo = authPassword.getAuthInfo(); + this.authValue = authPassword.getAuthValue(); + } + + protected String getExpectedAuthScheme() { + return null; + } + + protected boolean matchAuthValue(Object object) { + return authValue.equals(object.toString()); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AuthPassword)) + return false; + AuthPassword authPassword = (AuthPassword) obj; + return authScheme.equals(authPassword.authScheme) && authInfo.equals(authPassword.authInfo) + && authValue.equals(authValue); + } + + public boolean keyEquals(AuthPassword authPassword) { + return authScheme.equals(authPassword.authScheme) && authInfo.equals(authPassword.authInfo); + } + + @Override + public int hashCode() { + return authValue.hashCode(); + } + + @Override + public String toString() { + return toAuthPassword(); + } + + public final String toAuthPassword() { + return getAuthScheme() + '$' + authInfo + '$' + authValue; + } + + public String getAuthScheme() { + return authScheme; + } + + public String getAuthInfo() { + return authInfo; + } + + public String getAuthValue() { + return authValue; + } + + public static AuthPassword matchAuthValue(Attributes attributes, char[] value) { + try { + Attribute authPassword = attributes.get(LdapAttrs.authPassword.name()); + if (authPassword != null) { + NamingEnumeration values = authPassword.getAll(); + while (values.hasMore()) { + Object val = values.next(); + AuthPassword token = new AuthPassword(val.toString()); + String auth; + if (Arrays.binarySearch(value, '$') >= 0) { + auth = token.authInfo + '$' + token.authValue; + } else { + auth = token.authValue; + } + if (Arrays.equals(auth.toCharArray(), value)) + return token; + // if (token.matchAuthValue(value)) + // return token; + } + } + return null; + } catch (NamingException e) { + throw new IllegalStateException("Cannot check attribute", e); + } + } + + public static boolean remove(Attributes attributes, AuthPassword value) { + Attribute authPassword = attributes.get(LdapAttrs.authPassword.name()); + return authPassword.remove(value.toAuthPassword()); + } + + @Override + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (Callback callback : callbacks) { + if (callback instanceof NameCallback) + ((NameCallback) callback).setName(toAuthPassword()); + else if (callback instanceof PasswordCallback) + ((PasswordCallback) callback).setPassword(getAuthValue().toCharArray()); + } + } + +} diff --git a/org.argeo.util/src/org/argeo/util/naming/ldap/LdifParser.java b/org.argeo.util/src/org/argeo/util/naming/ldap/LdifParser.java new file mode 100644 index 000000000..3c4ae0a85 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/naming/ldap/LdifParser.java @@ -0,0 +1,161 @@ +package org.argeo.util.naming.ldap; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +import javax.naming.InvalidNameException; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttribute; +import javax.naming.directory.BasicAttributes; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; + +import org.argeo.util.naming.LdapAttrs; + +/** Basic LDIF parser. */ +public class LdifParser { + private final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + protected Attributes addAttributes(SortedMap res, int lineNumber, LdapName currentDn, + Attributes currentAttributes) { + try { + Rdn nameRdn = currentDn.getRdn(currentDn.size() - 1); + Attribute nameAttr = currentAttributes.get(nameRdn.getType()); + if (nameAttr == null) + currentAttributes.put(nameRdn.getType(), nameRdn.getValue()); + else if (!nameAttr.get().equals(nameRdn.getValue())) + throw new IllegalStateException( + "Attribute " + nameAttr.getID() + "=" + nameAttr.get() + " not consistent with DN " + currentDn + + " (shortly before line " + lineNumber + " in LDIF file)"); + Attributes previous = res.put(currentDn, currentAttributes); + return previous; + } catch (NamingException e) { + throw new IllegalStateException("Cannot add " + currentDn, e); + } + } + + /** With UTF-8 charset */ + public SortedMap read(InputStream in) throws IOException { + try (Reader reader = new InputStreamReader(in, DEFAULT_CHARSET)) { + return read(reader); + } finally { + try { + in.close(); + } catch (IOException e) { + // silent + } + } + } + + /** Will close the reader. */ + public SortedMap read(Reader reader) throws IOException { + SortedMap res = new TreeMap(); + try { + List lines = new ArrayList<>(); + try (BufferedReader br = new BufferedReader(reader)) { + String line; + while ((line = br.readLine()) != null) { + lines.add(line); + } + } + if (lines.size() == 0) + return res; + // add an empty new line since the last line is not checked + if (!lines.get(lines.size() - 1).equals("")) + lines.add(""); + + LdapName currentDn = null; + Attributes currentAttributes = null; + StringBuilder currentEntry = new StringBuilder(); + + readLines: for (int lineNumber = 0; lineNumber < lines.size(); lineNumber++) { + String line = lines.get(lineNumber); + boolean isLastLine = false; + if (lineNumber == lines.size() - 1) + isLastLine = true; + if (line.startsWith(" ")) { + currentEntry.append(line.substring(1)); + if (!isLastLine) + continue readLines; + } + + if (currentEntry.length() != 0 || isLastLine) { + // read previous attribute + StringBuilder attrId = new StringBuilder(8); + boolean isBase64 = false; + readAttrId: for (int i = 0; i < currentEntry.length(); i++) { + char c = currentEntry.charAt(i); + if (c == ':') { + if (i + 1 < currentEntry.length() && currentEntry.charAt(i + 1) == ':') + isBase64 = true; + currentEntry.delete(0, i + (isBase64 ? 2 : 1)); + break readAttrId; + } else { + attrId.append(c); + } + } + + String attributeId = attrId.toString(); + // TODO should we really trim the end of the string as well? + String cleanValueStr = currentEntry.toString().trim(); + Object attributeValue = isBase64 ? Base64.getDecoder().decode(cleanValueStr) : cleanValueStr; + + // manage DN attributes + if (attributeId.equals(LdapAttrs.DN) || isLastLine) { + if (currentDn != null) { + // + // ADD + // + Attributes previous = addAttributes(res, lineNumber, currentDn, currentAttributes); + if (previous != null) { +// log.warn("There was already an entry with DN " + currentDn +// + ", which has been discarded by a subsequent one."); + } + } + + if (attributeId.equals(LdapAttrs.DN)) + try { + currentDn = new LdapName(attributeValue.toString()); + currentAttributes = new BasicAttributes(true); + } catch (InvalidNameException e) { +// log.error(attributeValue + " not a valid DN, skipping the entry."); + currentDn = null; + currentAttributes = null; + } + } + + // store attribute + if (currentAttributes != null) { + Attribute attribute = currentAttributes.get(attributeId); + if (attribute == null) { + attribute = new BasicAttribute(attributeId); + currentAttributes.put(attribute); + } + attribute.add(attributeValue); + } + currentEntry = new StringBuilder(); + } + currentEntry.append(line); + } + } finally { + try { + reader.close(); + } catch (IOException e) { + // silent + } + } + return res; + } +} \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/util/naming/ldap/LdifWriter.java b/org.argeo.util/src/org/argeo/util/naming/ldap/LdifWriter.java new file mode 100644 index 000000000..3e25dcfcb --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/naming/ldap/LdifWriter.java @@ -0,0 +1,104 @@ +package org.argeo.util.naming.ldap; + +import static org.argeo.util.naming.LdapAttrs.DN; +import static org.argeo.util.naming.LdapAttrs.member; +import static org.argeo.util.naming.LdapAttrs.objectClass; +import static org.argeo.util.naming.LdapAttrs.uniqueMember; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; + +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; + +/** Basic LDIF writer */ +public class LdifWriter { + private final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + private final Writer writer; + + /** Writer must be closed by caller */ + public LdifWriter(Writer writer) { + this.writer = writer; + } + + /** Stream must be closed by caller */ + public LdifWriter(OutputStream out) { + this(new OutputStreamWriter(out, DEFAULT_CHARSET)); + } + + public void writeEntry(LdapName name, Attributes attributes) throws IOException { + try { + // check consistency + Rdn nameRdn = name.getRdn(name.size() - 1); + Attribute nameAttr = attributes.get(nameRdn.getType()); + if (!nameAttr.get().equals(nameRdn.getValue())) + throw new IllegalArgumentException( + "Attribute " + nameAttr.getID() + "=" + nameAttr.get() + " not consistent with DN " + name); + + writer.append(DN + ": ").append(name.toString()).append('\n'); + Attribute objectClassAttr = attributes.get(objectClass.name()); + if (objectClassAttr != null) + writeAttribute(objectClassAttr); + attributes: for (NamingEnumeration attrs = attributes.getAll(); attrs.hasMore();) { + Attribute attribute = attrs.next(); + if (attribute.getID().equals(DN) || attribute.getID().equals(objectClass.name())) + continue attributes;// skip DN attribute + if (attribute.getID().equals(member.name()) || attribute.getID().equals(uniqueMember.name())) + continue attributes;// skip member and uniqueMember attributes, so that they are always written last + writeAttribute(attribute); + } + // write member and uniqueMember attributes last + for (NamingEnumeration attrs = attributes.getAll(); attrs.hasMore();) { + Attribute attribute = attrs.next(); + if (attribute.getID().equals(member.name()) || attribute.getID().equals(uniqueMember.name())) + writeMemberAttribute(attribute); + } + writer.append('\n'); + writer.flush(); + } catch (NamingException e) { + throw new IllegalStateException("Cannot write LDIF", e); + } + } + + public void write(Map entries) throws IOException { + for (LdapName dn : entries.keySet()) + writeEntry(dn, entries.get(dn)); + } + + protected void writeAttribute(Attribute attribute) throws NamingException, IOException { + for (NamingEnumeration attrValues = attribute.getAll(); attrValues.hasMore();) { + Object value = attrValues.next(); + if (value instanceof byte[]) { + String encoded = Base64.getEncoder().encodeToString((byte[]) value); + writer.append(attribute.getID()).append(":: ").append(encoded).append('\n'); + } else { + writer.append(attribute.getID()).append(": ").append(value.toString()).append('\n'); + } + } + } + + protected void writeMemberAttribute(Attribute attribute) throws NamingException, IOException { + // Note: duplicate entries will be swallowed + SortedSet values = new TreeSet<>(); + for (NamingEnumeration attrValues = attribute.getAll(); attrValues.hasMore();) { + String value = attrValues.next().toString(); + values.add(value); + } + + for (String value : values) { + writer.append(attribute.getID()).append(": ").append(value).append('\n'); + } + } +} diff --git a/rap/org.argeo.cms.ui.rap/src/org/argeo/cms/web/AbstractCmsEntryPoint.java b/rap/org.argeo.cms.ui.rap/src/org/argeo/cms/web/AbstractCmsEntryPoint.java index 947a4d11a..36ed3da7a 100644 --- a/rap/org.argeo.cms.ui.rap/src/org/argeo/cms/web/AbstractCmsEntryPoint.java +++ b/rap/org.argeo.cms.ui.rap/src/org/argeo/cms/web/AbstractCmsEntryPoint.java @@ -34,8 +34,8 @@ import org.argeo.cms.swt.CmsStyles; import org.argeo.cms.swt.CmsSwtUtils; import org.argeo.eclipse.ui.specific.UiContext; import org.argeo.jcr.JcrUtils; -import org.argeo.util.naming.AuthPassword; import org.argeo.util.naming.SharedSecret; +import org.argeo.util.naming.ldap.AuthPassword; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.application.AbstractEntryPoint; import org.eclipse.rap.rwt.client.WebClient;