From: Mathieu Baudier Date: Wed, 5 Jan 2022 06:47:53 +0000 (+0100) Subject: Rename enterprise into util X-Git-Tag: argeo-commons-2.3.5~104 X-Git-Url: https://git.argeo.org/?a=commitdiff_plain;h=9f729eeb8255a9d800ad2506735dda8cc215a135;p=lgpl%2Fargeo-commons.git Rename enterprise into util --- diff --git a/demo/pom.xml b/demo/pom.xml deleted file mode 100644 index f152523a1..000000000 --- a/demo/pom.xml +++ /dev/null @@ -1,72 +0,0 @@ - - - 4.0.0 - - org.argeo.commons - argeo-commons - 2.3-SNAPSHOT - .. - - demo - Commons Demo - pom - - - - - org.argeo.tp.equinox - org.eclipse.osgi - - - org.argeo.commons - org.argeo.osgi.boot - 2.3-SNAPSHOT - - - - - argeo_node_rap - - - - org.argeo.maven.plugins - argeo-osgi-plugin - - argeo_node_rap.properties - exec/argeo_node_rap - - - - - - - org.argeo.commons - org.argeo.dep.cms.e4.rap - 2.3-SNAPSHOT - - - - - cms-e4-rap - - - - org.argeo.maven.plugins - argeo-osgi-plugin - - cms-e4-rap.properties - exec/cms-e4-rap - - - - - - - org.argeo.commons - org.argeo.dep.cms.e4.rap - 2.3-SNAPSHOT - - - - - \ No newline at end of file diff --git a/dep/org.argeo.dep.cms.minimal/pom.xml b/dep/org.argeo.dep.cms.minimal/pom.xml index 1594c0a24..9042eabab 100644 --- a/dep/org.argeo.dep.cms.minimal/pom.xml +++ b/dep/org.argeo.dep.cms.minimal/pom.xml @@ -21,7 +21,7 @@ org.argeo.commons - org.argeo.enterprise + org.argeo.util 2.3-SNAPSHOT diff --git a/dist/argeo-init/assembly/osgi-boot.xml b/dist/argeo-init/assembly/osgi-boot.xml new file mode 100644 index 000000000..adad22b8a --- /dev/null +++ b/dist/argeo-init/assembly/osgi-boot.xml @@ -0,0 +1,34 @@ + + dist + + + zip + + + + base/bin + bin + 0755 + + * + + + offline.sh + + + + + + false + ${artifact.artifactId}.${artifact.extension} + share/osgi/boot + + org.argeo.tp.equinox:org.eclipse.osgi + org.argeo.commons:org.argeo.osgi.boot + + + + \ No newline at end of file diff --git a/dist/argeo-init/base/bin/a2sh b/dist/argeo-init/base/bin/a2sh new file mode 100755 index 000000000..dce546313 --- /dev/null +++ b/dist/argeo-init/base/bin/a2sh @@ -0,0 +1,6 @@ +#!/bin/sh +BIN_DIR=`dirname $0` +EQUINOX=$BIN_DIR/../share/osgi/boot/org.eclipse.osgi.jar +OSGI_BOOT=$BIN_DIR/../share/osgi/boot/org.argeo.osgi.boot.jar + +/usr/bin/jshell --class-path "$EQUINOX:$OSGI_BOOT" $* diff --git a/dist/argeo-init/pom.xml b/dist/argeo-init/pom.xml new file mode 100644 index 000000000..39f3dd3ed --- /dev/null +++ b/dist/argeo-init/pom.xml @@ -0,0 +1,170 @@ + + 4.0.0 + + org.argeo.commons + 2.3-SNAPSHOT + dist + .. + + osgi-boot + pom + Commons Deployable OSGi Boot + + + + + + org.argeo.tp + argeo-tp + ${version.argeo-tp} + + + + + org.argeo.commons + org.argeo.init + 2.3-SNAPSHOT + + + + + dist + + + + org.apache.maven.plugins + maven-assembly-plugin + + osgi-boot-${version.released} + false + + assembly/osgi-boot.xml + + + + + assembly-base + package + + single + + + + + + + + + rpmbuild + + + + org.codehaus.mojo + rpm-maven-plugin + + + rpm-osgi-boot + package + + rpm + + + argeo-init${argeo.rpm.suffix} + + + /usr/bin + root + root + 755 + false + + + rpm/usr/bin + + * + + + + + + /usr/share/osgi/boot + root + root + 644 + false + + true + + org.argeo.commons:org.argeo.osgi.boot + + + + + /usr/share/osgi/boot + root + root + 644 + noreplace + false + + + rpm/usr/share/osgi/boot + + *.args + + + + + + + argeo-init-tp-equinox${argeo.rpm.suffix} + + + + + + + + + + rpmbuild-tp + + + + org.codehaus.mojo + rpm-maven-plugin + + + rpm-osgi-boot-equinox + package + + rpm + + + argeo-init-tp-equinox${argeo.rpm.suffix} + ${version.argeo-tp} + ${argeo.rpm.release.tp} + + + /usr/share/osgi/boot + root + root + 644 + false + + true + + org.argeo.tp.equinox:org.eclipse.osgi + + + + + + + + + + + + + diff --git a/dist/argeo-init/rpm/usr/bin/a2sh b/dist/argeo-init/rpm/usr/bin/a2sh new file mode 100755 index 000000000..07a27eb4b --- /dev/null +++ b/dist/argeo-init/rpm/usr/bin/a2sh @@ -0,0 +1,13 @@ +#!/bin/sh + +export A2_HOME=$HOME/.a2 +if [ -d "$A2_HOME/share/osgi/boot" ]; then + PREFIX=$A2_HOME +else + PREFIX=/usr +fi + +EQUINOX=$PREFIX/share/osgi/boot/org.eclipse.osgi.jar +OSGI_BOOT=$PREFIX/share/osgi/boot/org.argeo.osgi.boot.jar + +/usr/bin/jshell --class-path "$EQUINOX:$OSGI_BOOT" $* diff --git a/dist/argeo-init/rpm/usr/share/osgi/boot/framework.args b/dist/argeo-init/rpm/usr/share/osgi/boot/framework.args new file mode 100644 index 000000000..1c1d2fac8 --- /dev/null +++ b/dist/argeo-init/rpm/usr/share/osgi/boot/framework.args @@ -0,0 +1 @@ +-jar /usr/share/osgi/boot/org.eclipse.osgi.jar \ No newline at end of file diff --git a/dist/argeo-node/pom.xml b/dist/argeo-node/pom.xml index c066a6481..e1c8b2163 100644 --- a/dist/argeo-node/pom.xml +++ b/dist/argeo-node/pom.xml @@ -183,7 +183,7 @@ argeo-cms-node${argeo.rpm.suffix} - argeo-osgi-boot${argeo.rpm.suffix} + argeo-init${argeo.rpm.suffix} diff --git a/dist/osgi-boot/assembly/osgi-boot.xml b/dist/osgi-boot/assembly/osgi-boot.xml deleted file mode 100644 index adad22b8a..000000000 --- a/dist/osgi-boot/assembly/osgi-boot.xml +++ /dev/null @@ -1,34 +0,0 @@ - - dist - - - zip - - - - base/bin - bin - 0755 - - * - - - offline.sh - - - - - - false - ${artifact.artifactId}.${artifact.extension} - share/osgi/boot - - org.argeo.tp.equinox:org.eclipse.osgi - org.argeo.commons:org.argeo.osgi.boot - - - - \ No newline at end of file diff --git a/dist/osgi-boot/base/bin/a2sh b/dist/osgi-boot/base/bin/a2sh deleted file mode 100755 index dce546313..000000000 --- a/dist/osgi-boot/base/bin/a2sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -BIN_DIR=`dirname $0` -EQUINOX=$BIN_DIR/../share/osgi/boot/org.eclipse.osgi.jar -OSGI_BOOT=$BIN_DIR/../share/osgi/boot/org.argeo.osgi.boot.jar - -/usr/bin/jshell --class-path "$EQUINOX:$OSGI_BOOT" $* diff --git a/dist/osgi-boot/pom.xml b/dist/osgi-boot/pom.xml deleted file mode 100644 index 021710088..000000000 --- a/dist/osgi-boot/pom.xml +++ /dev/null @@ -1,170 +0,0 @@ - - 4.0.0 - - org.argeo.commons - 2.3-SNAPSHOT - dist - .. - - osgi-boot - pom - Commons Deployable OSGi Boot - - - - - - org.argeo.tp - argeo-tp - ${version.argeo-tp} - - - - - org.argeo.commons - org.argeo.osgi.boot - 2.3-SNAPSHOT - - - - - dist - - - - org.apache.maven.plugins - maven-assembly-plugin - - osgi-boot-${version.released} - false - - assembly/osgi-boot.xml - - - - - assembly-base - package - - single - - - - - - - - - rpmbuild - - - - org.codehaus.mojo - rpm-maven-plugin - - - rpm-osgi-boot - package - - rpm - - - argeo-osgi-boot${argeo.rpm.suffix} - - - /usr/bin - root - root - 755 - false - - - rpm/usr/bin - - * - - - - - - /usr/share/osgi/boot - root - root - 644 - false - - true - - org.argeo.commons:org.argeo.osgi.boot - - - - - /usr/share/osgi/boot - root - root - 644 - noreplace - false - - - rpm/usr/share/osgi/boot - - *.args - - - - - - - argeo-osgi-boot-equinox${argeo.rpm.suffix} - - - - - - - - - - rpmbuild-tp - - - - org.codehaus.mojo - rpm-maven-plugin - - - rpm-osgi-boot-equinox - package - - rpm - - - argeo-osgi-boot-equinox${argeo.rpm.suffix} - ${version.argeo-tp} - ${argeo.rpm.release.tp} - - - /usr/share/osgi/boot - root - root - 644 - false - - true - - org.argeo.tp.equinox:org.eclipse.osgi - - - - - - - - - - - - - diff --git a/dist/osgi-boot/rpm/usr/bin/a2sh b/dist/osgi-boot/rpm/usr/bin/a2sh deleted file mode 100755 index 07a27eb4b..000000000 --- a/dist/osgi-boot/rpm/usr/bin/a2sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/sh - -export A2_HOME=$HOME/.a2 -if [ -d "$A2_HOME/share/osgi/boot" ]; then - PREFIX=$A2_HOME -else - PREFIX=/usr -fi - -EQUINOX=$PREFIX/share/osgi/boot/org.eclipse.osgi.jar -OSGI_BOOT=$PREFIX/share/osgi/boot/org.argeo.osgi.boot.jar - -/usr/bin/jshell --class-path "$EQUINOX:$OSGI_BOOT" $* diff --git a/dist/osgi-boot/rpm/usr/share/osgi/boot/framework.args b/dist/osgi-boot/rpm/usr/share/osgi/boot/framework.args deleted file mode 100644 index 1c1d2fac8..000000000 --- a/dist/osgi-boot/rpm/usr/share/osgi/boot/framework.args +++ /dev/null @@ -1 +0,0 @@ --jar /usr/share/osgi/boot/org.eclipse.osgi.jar \ No newline at end of file diff --git a/dist/pom.xml b/dist/pom.xml index 5ed36895c..65b186ab6 100644 --- a/dist/pom.xml +++ b/dist/pom.xml @@ -11,7 +11,7 @@ Commons Deployable Distributions pom - osgi-boot + argeo-init argeo-node diff --git a/org.argeo.api/pom.xml b/org.argeo.api/pom.xml index e8b2b78fd..82ad85552 100644 --- a/org.argeo.api/pom.xml +++ b/org.argeo.api/pom.xml @@ -8,7 +8,7 @@ .. org.argeo.api - CMS API + CMS APIs jar diff --git a/org.argeo.cms/pom.xml b/org.argeo.cms/pom.xml index d30d45e2e..237af520b 100644 --- a/org.argeo.cms/pom.xml +++ b/org.argeo.cms/pom.xml @@ -18,7 +18,7 @@ org.argeo.commons - org.argeo.enterprise + org.argeo.util 2.3-SNAPSHOT diff --git a/org.argeo.enterprise/.classpath b/org.argeo.enterprise/.classpath deleted file mode 100644 index 71eb16789..000000000 --- a/org.argeo.enterprise/.classpath +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/org.argeo.enterprise/.project b/org.argeo.enterprise/.project deleted file mode 100644 index 5de2f0acf..000000000 --- a/org.argeo.enterprise/.project +++ /dev/null @@ -1,28 +0,0 @@ - - - org.argeo.enterprise - - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.pde.ManifestBuilder - - - - - org.eclipse.pde.SchemaBuilder - - - - - - org.eclipse.jdt.core.javanature - org.eclipse.pde.PluginNature - - diff --git a/org.argeo.enterprise/META-INF/.gitignore b/org.argeo.enterprise/META-INF/.gitignore deleted file mode 100644 index 4854a41b9..000000000 --- a/org.argeo.enterprise/META-INF/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/MANIFEST.MF diff --git a/org.argeo.enterprise/bnd.bnd b/org.argeo.enterprise/bnd.bnd deleted file mode 100644 index 5f42f7786..000000000 --- a/org.argeo.enterprise/bnd.bnd +++ /dev/null @@ -1,6 +0,0 @@ -Bundle-Activator: org.argeo.osgi.internal.EnterpriseActivator -Bundle-ActivationPolicy: lazy - -Import-Package: org.osgi.*;version=0.0.0,\ -!org.apache.commons.logging,\ -* diff --git a/org.argeo.enterprise/build.properties b/org.argeo.enterprise/build.properties deleted file mode 100644 index ae2abc5ff..000000000 --- a/org.argeo.enterprise/build.properties +++ /dev/null @@ -1 +0,0 @@ -source.. = src/ \ No newline at end of file diff --git a/org.argeo.enterprise/pom.xml b/org.argeo.enterprise/pom.xml deleted file mode 100644 index 92cdebb08..000000000 --- a/org.argeo.enterprise/pom.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - 4.0.0 - - org.argeo.commons - argeo-commons - 2.3-SNAPSHOT - .. - - org.argeo.enterprise - Argeo Enterprise - \ No newline at end of file diff --git a/org.argeo.enterprise/src/org/argeo/ident/IdentClient.java b/org.argeo.enterprise/src/org/argeo/ident/IdentClient.java deleted file mode 100644 index c42da9778..000000000 --- a/org.argeo.enterprise/src/org/argeo/ident/IdentClient.java +++ /dev/null @@ -1,124 +0,0 @@ -package org.argeo.ident; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.ConnectException; -import java.net.Socket; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.List; -import java.util.StringTokenizer; - -/** - * A simple ident client, supporting authd OpenSSL encrypted username. - * - * @see RFC 1413 https://tools.ietf.org/html/rfc1413 - */ -public class IdentClient { - public final static int DEFAULT_IDENT_PORT = 113; - public final static String AUTHD_PASSPHRASE_PATH = "/etc/ident.key"; - final static String NO_USER = "NO-USER"; - - private final String host; - private final int port; - - private OpenSslDecryptor openSslDecryptor = new OpenSslDecryptor(); - private String identPassphrase = null; - - public IdentClient(String host) { - this(host, readPassphrase(AUTHD_PASSPHRASE_PATH), DEFAULT_IDENT_PORT); - } - - public IdentClient(String host, Path passPhrasePath) { - this(host, readPassphrase(passPhrasePath), DEFAULT_IDENT_PORT); - } - - public IdentClient(String host, String identPassphrase) { - this(host, identPassphrase, DEFAULT_IDENT_PORT); - } - - public IdentClient(String host, String identPassphrase, int port) { - this.host = host; - this.identPassphrase = identPassphrase; - this.port = port; - } - - /** @return the username or null if none */ - public String getUsername(int serverPort, int clientPort) { - String answer; - try (Socket socket = new Socket(host, port)) { - String msg = clientPort + "," + serverPort + "\n"; - OutputStream out = socket.getOutputStream(); - out.write(msg.getBytes(StandardCharsets.US_ASCII)); - out.flush(); - BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); - answer = reader.readLine(); - } catch (ConnectException e) { - System.err.println( - "Ident client is configured but no ident server available on " + host + " (port " + port + ")"); - return null; - } catch (Exception e) { - throw new RuntimeException("Cannot read from ident server on " + host + " (port " + port + ")", e); - } - StringTokenizer st = new StringTokenizer(answer, " :\n"); - String username = null; - while (st.hasMoreTokens()) - username = st.nextToken(); - - if (username.equals(NO_USER)) - return null; - - if (identPassphrase != null && username.startsWith("[")) { - String encrypted = username.substring(1, username.length() - 1); - username = openSslDecryptor.decryptAuthd(encrypted, identPassphrase).trim(); - } -// System.out.println(username); - return username; - } - - public void setOpenSslDecryptor(OpenSslDecryptor openSslDecryptor) { - this.openSslDecryptor = openSslDecryptor; - } - - public static String readPassphrase(String filePath) { - return readPassphrase(Paths.get(filePath)); - } - - /** @return the first line of the file. */ - public static String readPassphrase(Path path) { - if (!isPathAvailable(path)) - return null; - List lines; - try { - lines = Files.readAllLines(path); - } catch (IOException e) { - throw new IllegalStateException("Cannot read " + path, e); - } - if (lines.size() == 0) - return null; - String passphrase = lines.get(0); - return passphrase; - } - - public static boolean isDefaultAuthdPassphraseFileAvailable() { - return isPathAvailable(Paths.get(AUTHD_PASSPHRASE_PATH)); - } - - public static boolean isPathAvailable(Path path) { - if (!Files.exists(path)) - return false; - if (!Files.isReadable(path)) - return false; - return true; - } - - public static void main(String[] args) { - IdentClient identClient = new IdentClient("127.0.0.1", "changeit"); - String username = identClient.getUsername(7070, 55958); - System.out.println(username); - } -} diff --git a/org.argeo.enterprise/src/org/argeo/ident/OpenSslDecryptor.java b/org.argeo.enterprise/src/org/argeo/ident/OpenSslDecryptor.java deleted file mode 100644 index 702b09bfe..000000000 --- a/org.argeo.enterprise/src/org/argeo/ident/OpenSslDecryptor.java +++ /dev/null @@ -1,162 +0,0 @@ -package org.argeo.ident; - -import java.nio.charset.StandardCharsets; -import java.security.GeneralSecurityException; -import java.security.MessageDigest; -import java.util.Arrays; -import java.util.Base64; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; - -/** - * Decrypts OpenSSL encrypted data. - * - * From - * https://stackoverflow.com/questions/11783062/how-to-decrypt-file-in-java-encrypted-with-openssl-command-using-aes - * - * See also - * https://stackoverflow.com/questions/54171959/badpadding-exception-when-trying-to-decrypt-aes-based-encrypted-text/54173509#54173509 - * for new default message digest (not yet in CentOS 7 as of July 2019) - */ -public class OpenSslDecryptor { - private static final int INDEX_KEY = 0; - private static final int INDEX_IV = 1; - private static final int ITERATIONS = 1; - - private static final int SALT_OFFSET = 8; - private static final int SALT_SIZE = 8; - private static final int CIPHERTEXT_OFFSET = SALT_OFFSET + SALT_SIZE; - - /** In bits. */ - private final int keySize; - - private Cipher cipher; - private MessageDigest messageDigest; - - public OpenSslDecryptor() { - /* - * Changed to SHA-256 from OpenSSL v1.1.0 (see - * https://stackoverflow.com/questions/39637388/encryption-decryption-doesnt- - * work-well-between-two-different-openssl-versions) - */ - this(128, "MD5"); - } - - public OpenSslDecryptor(int keySize, String messageDigest) { - this.keySize = keySize; - try { - this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - this.messageDigest = MessageDigest.getInstance(messageDigest); - } catch (GeneralSecurityException e) { - throw new IllegalArgumentException("Cannot initialise decryptor", e); - } - } - - public String decryptAuthd(String dataBase64, String passphrase) { - try { - byte[] headerSaltAndCipherText = Base64.getDecoder().decode(dataBase64); - - boolean withSalt = true; - byte[] salt = withSalt ? Arrays.copyOfRange(headerSaltAndCipherText, SALT_OFFSET, SALT_OFFSET + SALT_SIZE) - : null; - byte[] encrypted = withSalt - ? Arrays.copyOfRange(headerSaltAndCipherText, CIPHERTEXT_OFFSET, headerSaltAndCipherText.length) - : headerSaltAndCipherText; - - final byte[][] keyAndIV = EVP_BytesToKey(keySize / Byte.SIZE, cipher.getBlockSize(), messageDigest, salt, - passphrase.getBytes(StandardCharsets.US_ASCII), ITERATIONS); - SecretKeySpec key = new SecretKeySpec(keyAndIV[INDEX_KEY], "AES"); - IvParameterSpec iv = new IvParameterSpec(keyAndIV[INDEX_IV]); - - cipher.init(Cipher.DECRYPT_MODE, key, iv); - byte[] decrypted = cipher.doFinal(encrypted); - - String answer = new String(decrypted, StandardCharsets.US_ASCII); - return answer; - } catch (BadPaddingException e) { - throw new IllegalStateException("Bad password, algorithm, mode or padding;" - + " no salt, wrong number of iterations or corrupted ciphertext.", e); - } catch (IllegalBlockSizeException e) { - throw new IllegalStateException("Bad algorithm, mode or corrupted (resized) ciphertext.", e); - } catch (GeneralSecurityException e) { - throw new IllegalStateException(e); - } - } - - private static byte[][] EVP_BytesToKey(int key_len, int iv_len, MessageDigest md, byte[] salt, byte[] data, - int count) { - byte[][] both = new byte[2][]; - byte[] key = new byte[key_len]; - int key_ix = 0; - byte[] iv = new byte[iv_len]; - int iv_ix = 0; - both[0] = key; - both[1] = iv; - byte[] md_buf = null; - int nkey = key_len; - int niv = iv_len; - int i = 0; - if (data == null) { - return both; - } - int addmd = 0; - for (;;) { - md.reset(); - if (addmd++ > 0) { - md.update(md_buf); - } - md.update(data); - if (null != salt) { - md.update(salt, 0, 8); - } - md_buf = md.digest(); - for (i = 1; i < count; i++) { - md.reset(); - md.update(md_buf); - md_buf = md.digest(); - } - i = 0; - if (nkey > 0) { - for (;;) { - if (nkey == 0) - break; - if (i == md_buf.length) - break; - key[key_ix++] = md_buf[i]; - nkey--; - i++; - } - } - if (niv > 0 && i != md_buf.length) { - for (;;) { - if (niv == 0) - break; - if (i == md_buf.length) - break; - iv[iv_ix++] = md_buf[i]; - niv--; - i++; - } - } - if (nkey == 0 && niv == 0) { - break; - } - } - for (i = 0; i < md_buf.length; i++) { - md_buf[i] = 0; - } - return both; - } - - public static void main(String[] args) { - String dataBase64 = args[0]; - String passphrase = args[1]; - OpenSslDecryptor decryptor = new OpenSslDecryptor(); - System.out.println(decryptor.decryptAuthd(dataBase64, passphrase)); - } - -} \ No newline at end of file diff --git a/org.argeo.enterprise/src/org/argeo/ident/package-info.java b/org.argeo.enterprise/src/org/argeo/ident/package-info.java deleted file mode 100644 index 35dd1a29e..000000000 --- a/org.argeo.enterprise/src/org/argeo/ident/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Ident authentication protocol support. */ -package org.argeo.ident; \ No newline at end of file diff --git a/org.argeo.enterprise/src/org/argeo/naming/AttributesDictionary.java b/org.argeo.enterprise/src/org/argeo/naming/AttributesDictionary.java deleted file mode 100644 index e04721610..000000000 --- a/org.argeo.enterprise/src/org/argeo/naming/AttributesDictionary.java +++ /dev/null @@ -1,171 +0,0 @@ -package org.argeo.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.enterprise/src/org/argeo/naming/AuthPassword.java b/org.argeo.enterprise/src/org/argeo/naming/AuthPassword.java deleted file mode 100644 index 6d4c62b3f..000000000 --- a/org.argeo.enterprise/src/org/argeo/naming/AuthPassword.java +++ /dev/null @@ -1,140 +0,0 @@ -package org.argeo.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; - -import org.argeo.osgi.useradmin.UserDirectoryException; - -/** 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 UserDirectoryException("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.enterprise/src/org/argeo/naming/Distinguished.java b/org.argeo.enterprise/src/org/argeo/naming/Distinguished.java deleted file mode 100644 index 8b9c4b92b..000000000 --- a/org.argeo.enterprise/src/org/argeo/naming/Distinguished.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.argeo.naming; - -import java.util.EnumSet; -import java.util.Set; -import java.util.TreeSet; - -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; - -/** - * An object that can be identified with an X.500 distinguished name. - * - * @see https://tools.ietf.org/html/rfc1779 - */ -public interface Distinguished { - /** The related distinguished name. */ - String dn(); - - /** The related distinguished name as an {@link LdapName}. */ - default LdapName distinguishedName() { - try { - return new LdapName(dn()); - } catch (InvalidNameException e) { - throw new IllegalArgumentException("Distinguished name " + dn() + " is not properly formatted.", e); - } - } - - /** List all DNs of an enumeration as strings. */ - static Set enumToDns(EnumSet enumSet) { - Set res = new TreeSet<>(); - for (Enum enm : enumSet) { - res.add(((Distinguished) enm).dn()); - } - return res; - } -} diff --git a/org.argeo.enterprise/src/org/argeo/naming/DnsBrowser.java b/org.argeo.enterprise/src/org/argeo/naming/DnsBrowser.java deleted file mode 100644 index d9358c083..000000000 --- a/org.argeo.enterprise/src/org/argeo/naming/DnsBrowser.java +++ /dev/null @@ -1,179 +0,0 @@ -package org.argeo.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.enterprise/src/org/argeo/naming/LdapAttrs.csv b/org.argeo.enterprise/src/org/argeo/naming/LdapAttrs.csv deleted file mode 100644 index 676d72720..000000000 --- a/org.argeo.enterprise/src/org/argeo/naming/LdapAttrs.csv +++ /dev/null @@ -1,129 +0,0 @@ -uid,,,0.9.2342.19200300.100.1.1,,RFC 4519 -mail,,,0.9.2342.19200300.100.1.3,,RFC 4524 -info,,,0.9.2342.19200300.100.1.4,,RFC 4524 -drink,,,0.9.2342.19200300.100.1.5,,RFC 4524 -roomNumber,,,0.9.2342.19200300.100.1.6,,RFC 4524 -photo,,,0.9.2342.19200300.100.1.7,,RFC 2798 -userClass,,,0.9.2342.19200300.100.1.8,,RFC 4524 -host,,,0.9.2342.19200300.100.1.9,,RFC 4524 -manager,,,0.9.2342.19200300.100.1.10,,RFC 4524 -documentIdentifier,,,0.9.2342.19200300.100.1.11,,RFC 4524 -documentTitle,,,0.9.2342.19200300.100.1.12,,RFC 4524 -documentVersion,,,0.9.2342.19200300.100.1.13,,RFC 4524 -documentAuthor,,,0.9.2342.19200300.100.1.14,,RFC 4524 -documentLocation,,,0.9.2342.19200300.100.1.15,,RFC 4524 -homePhone,,,0.9.2342.19200300.100.1.20,,RFC 4524 -secretary,,,0.9.2342.19200300.100.1.21,,RFC 4524 -dc,,,0.9.2342.19200300.100.1.25,,RFC 4519 -associatedDomain,,,0.9.2342.19200300.100.1.37,,RFC 4524 -associatedName,,,0.9.2342.19200300.100.1.38,,RFC 4524 -homePostalAddress,,,0.9.2342.19200300.100.1.39,,RFC 4524 -personalTitle,,,0.9.2342.19200300.100.1.40,,RFC 4524 -mobile,,,0.9.2342.19200300.100.1.41,,RFC 4524 -pager,,,0.9.2342.19200300.100.1.42,,RFC 4524 -co,,,0.9.2342.19200300.100.1.43,,RFC 4524 -uniqueIdentifier,,,0.9.2342.19200300.100.1.44,,RFC 4524 -organizationalStatus,,,0.9.2342.19200300.100.1.45,,RFC 4524 -buildingName,,,0.9.2342.19200300.100.1.48,,RFC 4524 -audio,,,0.9.2342.19200300.100.1.55,,RFC 2798 -documentPublisher,,,0.9.2342.19200300.100.1.56,,RFC 4524 -jpegPhoto,,,0.9.2342.19200300.100.1.60,,RFC 2798 -vendorName,,,1.3.6.1.1.4,,RFC 3045 -vendorVersion,,,1.3.6.1.1.5,,RFC 3045 -entryUUID,,,1.3.6.1.1.16.4,,RFC 4530 -entryDN,,,1.3.6.1.1.20,,RFC 5020 -labeledURI,,,1.3.6.1.4.1.250.1.57,,RFC 2798 -numSubordinates,,,1.3.6.1.4.1.453.16.2.103,,draft-ietf-boreham-numsubordinates -namingContexts,,,1.3.6.1.4.1.1466.101.120.5,,RFC 4512 -altServer,,,1.3.6.1.4.1.1466.101.120.6,,RFC 4512 -supportedExtension,,,1.3.6.1.4.1.1466.101.120.7,,RFC 4512 -supportedControl,,,1.3.6.1.4.1.1466.101.120.13,,RFC 4512 -supportedSASLMechanisms,,,1.3.6.1.4.1.1466.101.120.14,,RFC 4512 -supportedLDAPVersion,,,1.3.6.1.4.1.1466.101.120.15,,RFC 4512 -ldapSyntaxes,,,1.3.6.1.4.1.1466.101.120.16,,RFC 4512 -supportedAuthPasswordSchemes,,,1.3.6.1.4.1.4203.1.3.3,,RFC 3112 -authPassword,,,1.3.6.1.4.1.4203.1.3.4,,RFC 3112 -supportedFeatures,,,1.3.6.1.4.1.4203.1.3.5,,RFC 4512 -inheritable,,,1.3.6.1.4.1.7628.5.4.1,,draft-ietf-ldup-subentry -blockInheritance,,,1.3.6.1.4.1.7628.5.4.2,,draft-ietf-ldup-subentry -objectClass,,,2.5.4.0,,RFC 4512 -aliasedObjectName,,,2.5.4.1,,RFC 4512 -cn,,,2.5.4.3,,RFC 4519 -sn,,,2.5.4.4,,RFC 4519 -serialNumber,,,2.5.4.5,,RFC 4519 -c,,,2.5.4.6,,RFC 4519 -l,,,2.5.4.7,,RFC 4519 -st,,,2.5.4.8,,RFC 4519 -street,,,2.5.4.9,,RFC 4519 -o,,,2.5.4.10,,RFC 4519 -ou,,,2.5.4.11,,RFC 4519 -title,,,2.5.4.12,,RFC 4519 -description,,,2.5.4.13,,RFC 4519 -searchGuide,,,2.5.4.14,,RFC 4519 -businessCategory,,,2.5.4.15,,RFC 4519 -postalAddress,,,2.5.4.16,,RFC 4519 -postalCode,,,2.5.4.17,,RFC 4519 -postOfficeBox,,,2.5.4.18,,RFC 4519 -physicalDeliveryOfficeName,,,2.5.4.19,,RFC 4519 -telephoneNumber,,,2.5.4.20,,RFC 4519 -telexNumber,,,2.5.4.21,,RFC 4519 -teletexTerminalIdentifier,,,2.5.4.22,,RFC 4519 -facsimileTelephoneNumber,,,2.5.4.23,,RFC 4519 -x121Address,,,2.5.4.24,,RFC 4519 -internationalISDNNumber,,,2.5.4.25,,RFC 4519 -registeredAddress,,,2.5.4.26,,RFC 4519 -destinationIndicator,,,2.5.4.27,,RFC 4519 -preferredDeliveryMethod,,,2.5.4.28,,RFC 4519 -member,,,2.5.4.31,,RFC 4519 -owner,,,2.5.4.32,,RFC 4519 -roleOccupant,,,2.5.4.33,,RFC 4519 -seeAlso,,,2.5.4.34,,RFC 4519 -userPassword,,,2.5.4.35,,RFC 4519 -userCertificate,,,2.5.4.36,,RFC 4523 -cACertificate,,,2.5.4.37,,RFC 4523 -authorityRevocationList,,,2.5.4.38,,RFC 4523 -certificateRevocationList,,,2.5.4.39,,RFC 4523 -crossCertificatePair,,,2.5.4.40,,RFC 4523 -name,,,2.5.4.41,,RFC 4519 -givenName,,,2.5.4.42,,RFC 4519 -initials,,,2.5.4.43,,RFC 4519 -generationQualifier,,,2.5.4.44,,RFC 4519 -x500UniqueIdentifier,,,2.5.4.45,,RFC 4519 -dnQualifier,,,2.5.4.46,,RFC 4519 -enhancedSearchGuide,,,2.5.4.47,,RFC 4519 -distinguishedName,,,2.5.4.49,,RFC 4519 -uniqueMember,,,2.5.4.50,,RFC 4519 -houseIdentifier,,,2.5.4.51,,RFC 4519 -supportedAlgorithms,,,2.5.4.52,,RFC 4523 -deltaRevocationList,,,2.5.4.53,,RFC 4523 -createTimestamp,,,2.5.18.1,,RFC 4512 -modifyTimestamp,,,2.5.18.2,,RFC 4512 -creatorsName,,,2.5.18.3,,RFC 4512 -modifiersName,,,2.5.18.4,,RFC 4512 -subschemaSubentry,,,2.5.18.10,,RFC 4512 -dITStructureRules,,,2.5.21.1,,RFC 4512 -dITContentRules,,,2.5.21.2,,RFC 4512 -matchingRules,,,2.5.21.4,,RFC 4512 -attributeTypes,,,2.5.21.5,,RFC 4512 -objectClasses,,,2.5.21.6,,RFC 4512 -nameForms,,,2.5.21.7,,RFC 4512 -matchingRuleUse,,,2.5.21.8,,RFC 4512 -structuralObjectClass,,,2.5.21.9,,RFC 4512 -governingStructureRule,,,2.5.21.10,,RFC 4512 -carLicense,,,2.16.840.1.113730.3.1.1,,RFC 2798 -departmentNumber,,,2.16.840.1.113730.3.1.2,,RFC 2798 -employeeNumber,,,2.16.840.1.113730.3.1.3,,RFC 2798 -employeeType,,,2.16.840.1.113730.3.1.4,,RFC 2798 -changeNumber,,,2.16.840.1.113730.3.1.5,,draft-good-ldap-changelog -targetDN,,,2.16.840.1.113730.3.1.6,,draft-good-ldap-changelog -changeType,,,2.16.840.1.113730.3.1.7,,draft-good-ldap-changelog -changes,,,2.16.840.1.113730.3.1.8,,draft-good-ldap-changelog -newRDN,,,2.16.840.1.113730.3.1.9,,draft-good-ldap-changelog -deleteOldRDN,,,2.16.840.1.113730.3.1.10,,draft-good-ldap-changelog -newSuperior,,,2.16.840.1.113730.3.1.11,,draft-good-ldap-changelog -ref,,,2.16.840.1.113730.3.1.34,,RFC 3296 -changelog,,,2.16.840.1.113730.3.1.35,,draft-good-ldap-changelog -preferredLanguage,,,2.16.840.1.113730.3.1.39,,RFC 2798 -userSMIMECertificate,,,2.16.840.1.113730.3.1.40,,RFC 2798 -userPKCS12,,,2.16.840.1.113730.3.1.216,,RFC 2798 -displayName,,,2.16.840.1.113730.3.1.241,,RFC 2798 diff --git a/org.argeo.enterprise/src/org/argeo/naming/LdapAttrs.java b/org.argeo.enterprise/src/org/argeo/naming/LdapAttrs.java deleted file mode 100644 index cfabeb7d6..000000000 --- a/org.argeo.enterprise/src/org/argeo/naming/LdapAttrs.java +++ /dev/null @@ -1,341 +0,0 @@ -package org.argeo.naming; - -/** - * Standard LDAP attributes as per:
- * - Standard LDAP
- * - Kerberos - * LDAP (partial) - */ -public enum LdapAttrs implements SpecifiedName { - /** */ - uid("0.9.2342.19200300.100.1.1", "RFC 4519"), - /** */ - mail("0.9.2342.19200300.100.1.3", "RFC 4524"), - /** */ - info("0.9.2342.19200300.100.1.4", "RFC 4524"), - /** */ - drink("0.9.2342.19200300.100.1.5", "RFC 4524"), - /** */ - roomNumber("0.9.2342.19200300.100.1.6", "RFC 4524"), - /** */ - photo("0.9.2342.19200300.100.1.7", "RFC 2798"), - /** */ - userClass("0.9.2342.19200300.100.1.8", "RFC 4524"), - /** */ - host("0.9.2342.19200300.100.1.9", "RFC 4524"), - /** */ - manager("0.9.2342.19200300.100.1.10", "RFC 4524"), - /** */ - documentIdentifier("0.9.2342.19200300.100.1.11", "RFC 4524"), - /** */ - documentTitle("0.9.2342.19200300.100.1.12", "RFC 4524"), - /** */ - documentVersion("0.9.2342.19200300.100.1.13", "RFC 4524"), - /** */ - documentAuthor("0.9.2342.19200300.100.1.14", "RFC 4524"), - /** */ - documentLocation("0.9.2342.19200300.100.1.15", "RFC 4524"), - /** */ - homePhone("0.9.2342.19200300.100.1.20", "RFC 4524"), - /** */ - secretary("0.9.2342.19200300.100.1.21", "RFC 4524"), - /** */ - dc("0.9.2342.19200300.100.1.25", "RFC 4519"), - /** */ - associatedDomain("0.9.2342.19200300.100.1.37", "RFC 4524"), - /** */ - associatedName("0.9.2342.19200300.100.1.38", "RFC 4524"), - /** */ - homePostalAddress("0.9.2342.19200300.100.1.39", "RFC 4524"), - /** */ - personalTitle("0.9.2342.19200300.100.1.40", "RFC 4524"), - /** */ - mobile("0.9.2342.19200300.100.1.41", "RFC 4524"), - /** */ - pager("0.9.2342.19200300.100.1.42", "RFC 4524"), - /** */ - co("0.9.2342.19200300.100.1.43", "RFC 4524"), - /** */ - uniqueIdentifier("0.9.2342.19200300.100.1.44", "RFC 4524"), - /** */ - organizationalStatus("0.9.2342.19200300.100.1.45", "RFC 4524"), - /** */ - buildingName("0.9.2342.19200300.100.1.48", "RFC 4524"), - /** */ - audio("0.9.2342.19200300.100.1.55", "RFC 2798"), - /** */ - documentPublisher("0.9.2342.19200300.100.1.56", "RFC 4524"), - /** */ - jpegPhoto("0.9.2342.19200300.100.1.60", "RFC 2798"), - /** */ - vendorName("1.3.6.1.1.4", "RFC 3045"), - /** */ - vendorVersion("1.3.6.1.1.5", "RFC 3045"), - /** */ - entryUUID("1.3.6.1.1.16.4", "RFC 4530"), - /** */ - entryDN("1.3.6.1.1.20", "RFC 5020"), - /** */ - labeledURI("1.3.6.1.4.1.250.1.57", "RFC 2798"), - /** */ - numSubordinates("1.3.6.1.4.1.453.16.2.103", "draft-ietf-boreham-numsubordinates"), - /** */ - namingContexts("1.3.6.1.4.1.1466.101.120.5", "RFC 4512"), - /** */ - altServer("1.3.6.1.4.1.1466.101.120.6", "RFC 4512"), - /** */ - supportedExtension("1.3.6.1.4.1.1466.101.120.7", "RFC 4512"), - /** */ - supportedControl("1.3.6.1.4.1.1466.101.120.13", "RFC 4512"), - /** */ - supportedSASLMechanisms("1.3.6.1.4.1.1466.101.120.14", "RFC 4512"), - /** */ - supportedLDAPVersion("1.3.6.1.4.1.1466.101.120.15", "RFC 4512"), - /** */ - ldapSyntaxes("1.3.6.1.4.1.1466.101.120.16", "RFC 4512"), - /** */ - supportedAuthPasswordSchemes("1.3.6.1.4.1.4203.1.3.3", "RFC 3112"), - /** */ - authPassword("1.3.6.1.4.1.4203.1.3.4", "RFC 3112"), - /** */ - supportedFeatures("1.3.6.1.4.1.4203.1.3.5", "RFC 4512"), - /** */ - inheritable("1.3.6.1.4.1.7628.5.4.1", "draft-ietf-ldup-subentry"), - /** */ - blockInheritance("1.3.6.1.4.1.7628.5.4.2", "draft-ietf-ldup-subentry"), - /** */ - objectClass("2.5.4.0", "RFC 4512"), - /** */ - aliasedObjectName("2.5.4.1", "RFC 4512"), - /** */ - cn("2.5.4.3", "RFC 4519"), - /** */ - sn("2.5.4.4", "RFC 4519"), - /** */ - serialNumber("2.5.4.5", "RFC 4519"), - /** */ - c("2.5.4.6", "RFC 4519"), - /** */ - l("2.5.4.7", "RFC 4519"), - /** */ - st("2.5.4.8", "RFC 4519"), - /** */ - street("2.5.4.9", "RFC 4519"), - /** */ - o("2.5.4.10", "RFC 4519"), - /** */ - ou("2.5.4.11", "RFC 4519"), - /** */ - title("2.5.4.12", "RFC 4519"), - /** */ - description("2.5.4.13", "RFC 4519"), - /** */ - searchGuide("2.5.4.14", "RFC 4519"), - /** */ - businessCategory("2.5.4.15", "RFC 4519"), - /** */ - postalAddress("2.5.4.16", "RFC 4519"), - /** */ - postalCode("2.5.4.17", "RFC 4519"), - /** */ - postOfficeBox("2.5.4.18", "RFC 4519"), - /** */ - physicalDeliveryOfficeName("2.5.4.19", "RFC 4519"), - /** */ - telephoneNumber("2.5.4.20", "RFC 4519"), - /** */ - telexNumber("2.5.4.21", "RFC 4519"), - /** */ - teletexTerminalIdentifier("2.5.4.22", "RFC 4519"), - /** */ - facsimileTelephoneNumber("2.5.4.23", "RFC 4519"), - /** */ - x121Address("2.5.4.24", "RFC 4519"), - /** */ - internationalISDNNumber("2.5.4.25", "RFC 4519"), - /** */ - registeredAddress("2.5.4.26", "RFC 4519"), - /** */ - destinationIndicator("2.5.4.27", "RFC 4519"), - /** */ - preferredDeliveryMethod("2.5.4.28", "RFC 4519"), - /** */ - member("2.5.4.31", "RFC 4519"), - /** */ - owner("2.5.4.32", "RFC 4519"), - /** */ - roleOccupant("2.5.4.33", "RFC 4519"), - /** */ - seeAlso("2.5.4.34", "RFC 4519"), - /** */ - userPassword("2.5.4.35", "RFC 4519"), - /** */ - userCertificate("2.5.4.36", "RFC 4523"), - /** */ - cACertificate("2.5.4.37", "RFC 4523"), - /** */ - authorityRevocationList("2.5.4.38", "RFC 4523"), - /** */ - certificateRevocationList("2.5.4.39", "RFC 4523"), - /** */ - crossCertificatePair("2.5.4.40", "RFC 4523"), - /** */ - name("2.5.4.41", "RFC 4519"), - /** */ - givenName("2.5.4.42", "RFC 4519"), - /** */ - initials("2.5.4.43", "RFC 4519"), - /** */ - generationQualifier("2.5.4.44", "RFC 4519"), - /** */ - x500UniqueIdentifier("2.5.4.45", "RFC 4519"), - /** */ - dnQualifier("2.5.4.46", "RFC 4519"), - /** */ - enhancedSearchGuide("2.5.4.47", "RFC 4519"), - /** */ - distinguishedName("2.5.4.49", "RFC 4519"), - /** */ - uniqueMember("2.5.4.50", "RFC 4519"), - /** */ - houseIdentifier("2.5.4.51", "RFC 4519"), - /** */ - supportedAlgorithms("2.5.4.52", "RFC 4523"), - /** */ - deltaRevocationList("2.5.4.53", "RFC 4523"), - /** */ - createTimestamp("2.5.18.1", "RFC 4512"), - /** */ - modifyTimestamp("2.5.18.2", "RFC 4512"), - /** */ - creatorsName("2.5.18.3", "RFC 4512"), - /** */ - modifiersName("2.5.18.4", "RFC 4512"), - /** */ - subschemaSubentry("2.5.18.10", "RFC 4512"), - /** */ - dITStructureRules("2.5.21.1", "RFC 4512"), - /** */ - dITContentRules("2.5.21.2", "RFC 4512"), - /** */ - matchingRules("2.5.21.4", "RFC 4512"), - /** */ - attributeTypes("2.5.21.5", "RFC 4512"), - /** */ - objectClasses("2.5.21.6", "RFC 4512"), - /** */ - nameForms("2.5.21.7", "RFC 4512"), - /** */ - matchingRuleUse("2.5.21.8", "RFC 4512"), - /** */ - structuralObjectClass("2.5.21.9", "RFC 4512"), - /** */ - governingStructureRule("2.5.21.10", "RFC 4512"), - /** */ - carLicense("2.16.840.1.113730.3.1.1", "RFC 2798"), - /** */ - departmentNumber("2.16.840.1.113730.3.1.2", "RFC 2798"), - /** */ - employeeNumber("2.16.840.1.113730.3.1.3", "RFC 2798"), - /** */ - employeeType("2.16.840.1.113730.3.1.4", "RFC 2798"), - /** */ - changeNumber("2.16.840.1.113730.3.1.5", "draft-good-ldap-changelog"), - /** */ - targetDN("2.16.840.1.113730.3.1.6", "draft-good-ldap-changelog"), - /** */ - changeType("2.16.840.1.113730.3.1.7", "draft-good-ldap-changelog"), - /** */ - changes("2.16.840.1.113730.3.1.8", "draft-good-ldap-changelog"), - /** */ - newRDN("2.16.840.1.113730.3.1.9", "draft-good-ldap-changelog"), - /** */ - deleteOldRDN("2.16.840.1.113730.3.1.10", "draft-good-ldap-changelog"), - /** */ - newSuperior("2.16.840.1.113730.3.1.11", "draft-good-ldap-changelog"), - /** */ - ref("2.16.840.1.113730.3.1.34", "RFC 3296"), - /** */ - changelog("2.16.840.1.113730.3.1.35", "draft-good-ldap-changelog"), - /** */ - preferredLanguage("2.16.840.1.113730.3.1.39", "RFC 2798"), - /** */ - userSMIMECertificate("2.16.840.1.113730.3.1.40", "RFC 2798"), - /** */ - userPKCS12("2.16.840.1.113730.3.1.216", "RFC 2798"), - /** */ - displayName("2.16.840.1.113730.3.1.241", "RFC 2798"), - - // Sun memberOf - memberOf("1.2.840.113556.1.2.102", "389 DS memberOf"), - - // KERBEROS (partial) - krbPrincipalName("2.16.840.1.113719.1.301.6.8.1", "Novell Kerberos Schema Definitions"), - - // RFC 2985 and RFC 3039 (partial) - dateOfBirth("1.3.6.1.5.5.7.9.1", "RFC 2985"), - /** */ - placeOfBirth("1.3.6.1.5.5.7.9.2", "RFC 2985"), - /** */ - gender("1.3.6.1.5.5.7.9.3", "RFC 2985"), - /** */ - countryOfCitizenship("1.3.6.1.5.5.7.9.4", "RFC 2985"), - /** */ - countryOfResidence("1.3.6.1.5.5.7.9.5", "RFC 2985"), - // - ; - - public final static String DN = "dn"; - -// private final static String LDAP_ = "ldap:"; - - private final String oid, spec; - - LdapAttrs(String oid, String spec) { - this.oid = oid; - this.spec = spec; - } - - @Override - public String getID() { - return oid; - } - - @Override - public String getSpec() { - return spec; - } - - public String getPrefix() { - return prefix(); - } - - public static String prefix() { - return "ldap"; - } - - public String property() { - return qualified(); - } - - public String qualified() { - String prefix = getPrefix(); - return prefix != null ? prefix + ":" + name() : name(); - } - - public String getNamespace() { - return namespace(); - } - - public static String namespace() { - return "http://www.argeo.org/ns/ldap"; - } - - @Override - public final String toString() { - // must return the name - return name(); - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/naming/LdapObjs.csv b/org.argeo.enterprise/src/org/argeo/naming/LdapObjs.csv deleted file mode 100644 index 3d907cbeb..000000000 --- a/org.argeo.enterprise/src/org/argeo/naming/LdapObjs.csv +++ /dev/null @@ -1,42 +0,0 @@ -account,,,0.9.2342.19200300.100.4.5,,RFC 4524 -document,,,0.9.2342.19200300.100.4.6,,RFC 4524 -room,,,0.9.2342.19200300.100.4.7,,RFC 4524 -documentSeries,,,0.9.2342.19200300.100.4.9,,RFC 4524 -domain,,,0.9.2342.19200300.100.4.13,,RFC 4524 -rFC822localPart,,,0.9.2342.19200300.100.4.14,,RFC 4524 -domainRelatedObject,,,0.9.2342.19200300.100.4.17,,RFC 4524 -friendlyCountry,,,0.9.2342.19200300.100.4.18,,RFC 4524 -simpleSecurityObject,,,0.9.2342.19200300.100.4.19,,RFC 4524 -uidObject,,,1.3.6.1.1.3.1,,RFC 4519 -extensibleObject,,,1.3.6.1.4.1.1466.101.120.111,,RFC 4512 -dcObject,,,1.3.6.1.4.1.1466.344,,RFC 4519 -authPasswordObject,,,1.3.6.1.4.1.4203.1.4.7,,RFC 3112 -namedObject,,,1.3.6.1.4.1.5322.13.1.1,,draft-howard-namedobject -inheritableLDAPSubEntry,,,1.3.6.1.4.1.7628.5.6.1.1,,draft-ietf-ldup-subentry -top,,,2.5.6.0,,RFC 4512 -alias,,,2.5.6.1,,RFC 4512 -country,,,2.5.6.2,,RFC 4519 -locality,,,2.5.6.3,,RFC 4519 -organization,,,2.5.6.4,,RFC 4519 -organizationalUnit,,,2.5.6.5,,RFC 4519 -person,,,2.5.6.6,,RFC 4519 -organizationalPerson,,,2.5.6.7,,RFC 4519 -organizationalRole,,,2.5.6.8,,RFC 4519 -groupOfNames,,,2.5.6.9,,RFC 4519 -residentialPerson,,,2.5.6.10,,RFC 4519 -applicationProcess,,,2.5.6.11,,RFC 4519 -device,,,2.5.6.14,,RFC 4519 -strongAuthenticationUser,,,2.5.6.15,,RFC 4523 -certificationAuthority,,,2.5.6.16,,RFC 4523 -certificationAuthority-V2,,,2.5.6.16.2,,RFC 4523 -groupOfUniqueNames,,,2.5.6.17,,RFC 4519 -userSecurityInformation,,,2.5.6.18,,RFC 4523 -cRLDistributionPoint,,,2.5.6.19,,RFC 4523 -pkiUser,,,2.5.6.21,,RFC 4523 -pkiCA,,,2.5.6.22,,RFC 4523 -deltaCRL,,,2.5.6.23,,RFC 4523 -subschema,,,2.5.20.1,,RFC 4512 -ldapSubEntry,,,2.16.840.1.113719.2.142.6.1.1,,draft-ietf-ldup-subentry -changeLogEntry,,,2.16.840.1.113730.3.2.1,,draft-good-ldap-changelog -inetOrgPerson,,,2.16.840.1.113730.3.2.2,,RFC 2798 -referral,,,2.16.840.1.113730.3.2.6,,RFC 3296 diff --git a/org.argeo.enterprise/src/org/argeo/naming/LdapObjs.java b/org.argeo.enterprise/src/org/argeo/naming/LdapObjs.java deleted file mode 100644 index 061167513..000000000 --- a/org.argeo.enterprise/src/org/argeo/naming/LdapObjs.java +++ /dev/null @@ -1,114 +0,0 @@ -package org.argeo.naming; - -/** - * Standard LDAP object classes as per - * https://www.ldap.com/ldap- - * oid-reference - */ -public enum LdapObjs implements SpecifiedName { - account("0.9.2342.19200300.100.4.5", "RFC 4524"), - /** */ - document("0.9.2342.19200300.100.4.6", "RFC 4524"), - /** */ - room("0.9.2342.19200300.100.4.7", "RFC 4524"), - /** */ - documentSeries("0.9.2342.19200300.100.4.9", "RFC 4524"), - /** */ - domain("0.9.2342.19200300.100.4.13", "RFC 4524"), - /** */ - rFC822localPart("0.9.2342.19200300.100.4.14", "RFC 4524"), - /** */ - domainRelatedObject("0.9.2342.19200300.100.4.17", "RFC 4524"), - /** */ - friendlyCountry("0.9.2342.19200300.100.4.18", "RFC 4524"), - /** */ - simpleSecurityObject("0.9.2342.19200300.100.4.19", "RFC 4524"), - /** */ - uidObject("1.3.6.1.1.3.1", "RFC 4519"), - /** */ - extensibleObject("1.3.6.1.4.1.1466.101.120.111", "RFC 4512"), - /** */ - dcObject("1.3.6.1.4.1.1466.344", "RFC 4519"), - /** */ - authPasswordObject("1.3.6.1.4.1.4203.1.4.7", "RFC 3112"), - /** */ - namedObject("1.3.6.1.4.1.5322.13.1.1", "draft-howard-namedobject"), - /** */ - inheritableLDAPSubEntry("1.3.6.1.4.1.7628.5.6.1.1", "draft-ietf-ldup-subentry"), - /** */ - top("2.5.6.0", "RFC 4512"), - /** */ - alias("2.5.6.1", "RFC 4512"), - /** */ - country("2.5.6.2", "RFC 4519"), - /** */ - locality("2.5.6.3", "RFC 4519"), - /** */ - organization("2.5.6.4", "RFC 4519"), - /** */ - organizationalUnit("2.5.6.5", "RFC 4519"), - /** */ - person("2.5.6.6", "RFC 4519"), - /** */ - organizationalPerson("2.5.6.7", "RFC 4519"), - /** */ - organizationalRole("2.5.6.8", "RFC 4519"), - /** */ - groupOfNames("2.5.6.9", "RFC 4519"), - /** */ - residentialPerson("2.5.6.10", "RFC 4519"), - /** */ - applicationProcess("2.5.6.11", "RFC 4519"), - /** */ - device("2.5.6.14", "RFC 4519"), - /** */ - strongAuthenticationUser("2.5.6.15", "RFC 4523"), - /** */ - certificationAuthority("2.5.6.16", "RFC 4523"), - // /** Should be certificationAuthority-V2 */ - // certificationAuthority_V2("2.5.6.16.2", "RFC 4523") { - // }, - /** */ - groupOfUniqueNames("2.5.6.17", "RFC 4519"), - /** */ - userSecurityInformation("2.5.6.18", "RFC 4523"), - /** */ - cRLDistributionPoint("2.5.6.19", "RFC 4523"), - /** */ - pkiUser("2.5.6.21", "RFC 4523"), - /** */ - pkiCA("2.5.6.22", "RFC 4523"), - /** */ - deltaCRL("2.5.6.23", "RFC 4523"), - /** */ - subschema("2.5.20.1", "RFC 4512"), - /** */ - ldapSubEntry("2.16.840.1.113719.2.142.6.1.1", "draft-ietf-ldup-subentry"), - /** */ - changeLogEntry("2.16.840.1.113730.3.2.1", "draft-good-ldap-changelog"), - /** */ - inetOrgPerson("2.16.840.1.113730.3.2.2", "RFC 2798"), - /** */ - referral("2.16.840.1.113730.3.2.6", "RFC 3296"); - - private final static String LDAP_ = "ldap:"; - private final String oid, spec; - - private LdapObjs(String oid, String spec) { - this.oid = oid; - this.spec = spec; - } - - public String getOid() { - return oid; - } - - public String getSpec() { - return spec; - } - - public String property() { - return new StringBuilder(LDAP_).append(name()).toString(); - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/naming/LdifParser.java b/org.argeo.enterprise/src/org/argeo/naming/LdifParser.java deleted file mode 100644 index cc1957058..000000000 --- a/org.argeo.enterprise/src/org/argeo/naming/LdifParser.java +++ /dev/null @@ -1,161 +0,0 @@ -package org.argeo.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; - -import org.argeo.osgi.useradmin.UserDirectoryException; - -/** 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 UserDirectoryException( - "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 UserDirectoryException("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.enterprise/src/org/argeo/naming/LdifWriter.java b/org.argeo.enterprise/src/org/argeo/naming/LdifWriter.java deleted file mode 100644 index 98d2df055..000000000 --- a/org.argeo.enterprise/src/org/argeo/naming/LdifWriter.java +++ /dev/null @@ -1,106 +0,0 @@ -package org.argeo.naming; - -import static org.argeo.naming.LdapAttrs.DN; -import static org.argeo.naming.LdapAttrs.member; -import static org.argeo.naming.LdapAttrs.objectClass; -import static org.argeo.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; - -import org.argeo.osgi.useradmin.UserDirectoryException; - -/** 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 UserDirectoryException( - "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 UserDirectoryException("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.enterprise/src/org/argeo/naming/NamingUtils.java b/org.argeo.enterprise/src/org/argeo/naming/NamingUtils.java deleted file mode 100644 index 5a868ddb4..000000000 --- a/org.argeo.enterprise/src/org/argeo/naming/NamingUtils.java +++ /dev/null @@ -1,106 +0,0 @@ -package org.argeo.naming; - -import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.time.OffsetDateTime; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.time.temporal.ChronoField; -import java.util.Calendar; -import java.util.GregorianCalendar; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; - -public class NamingUtils { - /** As per https://tools.ietf.org/html/rfc4517#section-3.3.13 */ - private final static DateTimeFormatter utcLdapDate = DateTimeFormatter.ofPattern("uuuuMMddHHmmssX") - .withZone(ZoneOffset.UTC); - - /** @return null if not parseable */ - public static Instant ldapDateToInstant(String ldapDate) { - try { - return OffsetDateTime.parse(ldapDate, utcLdapDate).toInstant(); - } catch (DateTimeParseException e) { - return null; - } - } - - /** @return null if not parseable */ - public static ZonedDateTime ldapDateToZonedDateTime(String ldapDate) { - try { - return OffsetDateTime.parse(ldapDate, utcLdapDate).toZonedDateTime(); - } catch (DateTimeParseException e) { - return null; - } - } - - public static Calendar ldapDateToCalendar(String ldapDate) { - OffsetDateTime instant = OffsetDateTime.parse(ldapDate, utcLdapDate); - GregorianCalendar calendar = new GregorianCalendar(); - calendar.set(Calendar.DAY_OF_MONTH, instant.get(ChronoField.DAY_OF_MONTH)); - calendar.set(Calendar.MONTH, instant.get(ChronoField.MONTH_OF_YEAR)); - calendar.set(Calendar.YEAR, instant.get(ChronoField.YEAR)); - return calendar; - } - - public static String instantToLdapDate(ZonedDateTime instant) { - return utcLdapDate.format(instant.withZoneSameInstant(ZoneOffset.UTC)); - } - - public static String getQueryValue(Map> query, String key) { - if (!query.containsKey(key)) - return null; - List val = query.get(key); - if (val.size() == 1) - return val.get(0); - else - throw new IllegalArgumentException("There are " + val.size() + " value(s) for " + key); - } - - public static Map> queryToMap(URI uri) { - return queryToMap(uri.getQuery()); - } - - private static Map> queryToMap(String queryPart) { - try { - final Map> query_pairs = new LinkedHashMap>(); - if (queryPart == null) - return query_pairs; - final String[] pairs = queryPart.split("&"); - for (String pair : pairs) { - final int idx = pair.indexOf("="); - final String key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8.name()) - : pair; - if (!query_pairs.containsKey(key)) { - query_pairs.put(key, new LinkedList()); - } - final String value = idx > 0 && pair.length() > idx + 1 - ? URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8.name()) - : null; - query_pairs.get(key).add(value); - } - return query_pairs; - } catch (UnsupportedEncodingException e) { - throw new IllegalArgumentException("Cannot convert " + queryPart + " to map", e); - } - } - - private NamingUtils() { - - } - - public static void main(String args[]) { - ZonedDateTime now = ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC); - String str = utcLdapDate.format(now); - System.out.println(str); - utcLdapDate.parse(str); - utcLdapDate.parse("19520512000000Z"); - } -} diff --git a/org.argeo.enterprise/src/org/argeo/naming/SharedSecret.java b/org.argeo.enterprise/src/org/argeo/naming/SharedSecret.java deleted file mode 100644 index 369b411fc..000000000 --- a/org.argeo.enterprise/src/org/argeo/naming/SharedSecret.java +++ /dev/null @@ -1,46 +0,0 @@ -package org.argeo.naming; - -import java.time.Instant; -import java.time.ZoneOffset; -import java.time.ZonedDateTime; - -public class SharedSecret extends AuthPassword { - public final static String X_SHARED_SECRET = "X-SharedSecret"; - private final Instant expiry; - - public SharedSecret(String authInfo, String authValue) { - super(authInfo, authValue); - expiry = null; - } - - public SharedSecret(AuthPassword authPassword) { - super(authPassword); - String authInfo = getAuthInfo(); - if (authInfo.length() == 16) { - expiry = NamingUtils.ldapDateToInstant(authInfo); - } else { - expiry = null; - } - } - - public SharedSecret(ZonedDateTime expiryTimestamp, String value) { - super(NamingUtils.instantToLdapDate(expiryTimestamp), value); - expiry = expiryTimestamp.withZoneSameInstant(ZoneOffset.UTC).toInstant(); - } - - public SharedSecret(int hours, String value) { - this(ZonedDateTime.now().plusHours(hours), value); - } - - @Override - protected String getExpectedAuthScheme() { - return X_SHARED_SECRET; - } - - public boolean isExpired() { - if (expiry == null) - return false; - return expiry.isBefore(Instant.now()); - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/naming/SpecifiedName.java b/org.argeo.enterprise/src/org/argeo/naming/SpecifiedName.java deleted file mode 100644 index 28cc2f9da..000000000 --- a/org.argeo.enterprise/src/org/argeo/naming/SpecifiedName.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.argeo.naming; - -/** - * A name which has been specified and for which an id has been defined - * (typically an OID). - */ -public interface SpecifiedName { - /** The name */ - String name(); - - /** An RFC or the URLof some specification */ - default String getSpec() { - return null; - } - - /** Typically an OID */ - default String getID() { - return getClass().getName() + "." + name(); - } -} diff --git a/org.argeo.enterprise/src/org/argeo/naming/SrvRecord.java b/org.argeo.enterprise/src/org/argeo/naming/SrvRecord.java deleted file mode 100644 index 8ecc94457..000000000 --- a/org.argeo.enterprise/src/org/argeo/naming/SrvRecord.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.argeo.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.enterprise/src/org/argeo/naming/package-info.java b/org.argeo.enterprise/src/org/argeo/naming/package-info.java deleted file mode 100644 index 95e7de313..000000000 --- a/org.argeo.enterprise/src/org/argeo/naming/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Generic naming and LDAP support. */ -package org.argeo.naming; \ No newline at end of file diff --git a/org.argeo.enterprise/src/org/argeo/osgi/internal/EnterpriseActivator.java b/org.argeo.enterprise/src/org/argeo/osgi/internal/EnterpriseActivator.java deleted file mode 100644 index bb495dd12..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/internal/EnterpriseActivator.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.argeo.osgi.internal; - -import org.osgi.framework.BundleActivator; -import org.osgi.framework.BundleContext; - -/** - * Called to gather information about the OSGi runtime. Should not activate - * anything else that canonical monitoring services (not creating implicit - * APIs), which is the responsibility of higher levels. - */ -public class EnterpriseActivator implements BundleActivator { - - @Override - public void start(BundleContext context) throws Exception { - } - - @Override - public void stop(BundleContext context) throws Exception { - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/metatype/EnumAD.java b/org.argeo.enterprise/src/org/argeo/osgi/metatype/EnumAD.java deleted file mode 100644 index 44b429934..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/metatype/EnumAD.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.argeo.osgi.metatype; - -import org.argeo.naming.SpecifiedName; -import org.osgi.service.metatype.AttributeDefinition; - -public interface EnumAD extends SpecifiedName, AttributeDefinition { - String name(); - - default Object getDefault() { - return null; - } - - @Override - default String getName() { - return name(); - } - - @Override - default String getID() { - return getClass().getName() + "." + name(); - } - - @Override - default String getDescription() { - return null; - } - - @Override - default int getCardinality() { - return 0; - } - - @Override - default int getType() { - return STRING; - } - - @Override - default String[] getOptionValues() { - return null; - } - - @Override - default String[] getOptionLabels() { - return null; - } - - @Override - default String validate(String value) { - return null; - } - - @Override - default String[] getDefaultValue() { - Object value = getDefault(); - if (value == null) - return null; - return new String[] { value.toString() }; - } -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/metatype/EnumOCD.java b/org.argeo.enterprise/src/org/argeo/osgi/metatype/EnumOCD.java deleted file mode 100644 index 97c7d56e1..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/metatype/EnumOCD.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.argeo.osgi.metatype; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.EnumSet; -import java.util.List; - -import org.osgi.service.metatype.AttributeDefinition; -import org.osgi.service.metatype.ObjectClassDefinition; - -public class EnumOCD> implements ObjectClassDefinition { - private final Class enumClass; - private String locale; - - public EnumOCD(Class clazz, String locale) { - this.enumClass = clazz; - this.locale = locale; - } - - @Override - public String getName() { - return null; - } - - public String getLocale() { - return locale; - } - - @Override - public String getID() { - return enumClass.getName(); - } - - @Override - public String getDescription() { - return null; - } - - @Override - public AttributeDefinition[] getAttributeDefinitions(int filter) { - EnumSet set = EnumSet.allOf(enumClass); - List attrs = new ArrayList<>(); - for (T key : set) - attrs.add((AttributeDefinition) key); - return attrs.toArray(new AttributeDefinition[attrs.size()]); - } - - @Override - public InputStream getIcon(int size) throws IOException { - return null; - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/metatype/package-info.java b/org.argeo.enterprise/src/org/argeo/osgi/metatype/package-info.java deleted file mode 100644 index bca5d1fda..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/metatype/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** OSGi metatype support. */ -package org.argeo.osgi.metatype; \ No newline at end of file diff --git a/org.argeo.enterprise/src/org/argeo/osgi/provisioning/SimpleProvisioningService.java b/org.argeo.enterprise/src/org/argeo/osgi/provisioning/SimpleProvisioningService.java deleted file mode 100644 index c0ec29000..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/provisioning/SimpleProvisioningService.java +++ /dev/null @@ -1,118 +0,0 @@ -package org.argeo.osgi.provisioning; - -import java.io.IOException; -import java.util.Collections; -import java.util.Dictionary; -import java.util.Enumeration; -import java.util.Iterator; -import java.util.Map; -import java.util.TreeMap; -import java.util.zip.ZipInputStream; - -import org.osgi.service.provisioning.ProvisioningService; - -public class SimpleProvisioningService implements ProvisioningService { - private Map map = Collections.synchronizedSortedMap(new TreeMap<>()); - - public SimpleProvisioningService() { - // update count - map.put(PROVISIONING_UPDATE_COUNT, 0); - } - - @Override - public Dictionary getInformation() { - return new Information(); - } - - @Override - public synchronized void setInformation(Dictionary info) { - map.clear(); - addInformation(info); - } - - @Override - public synchronized void addInformation(Dictionary info) { - Enumeration e = info.keys(); - while (e.hasMoreElements()) { - String key = e.nextElement(); - map.put(key, info.get(key)); - } - incrementProvisioningUpdateCount(); - } - - protected synchronized void incrementProvisioningUpdateCount() { - Integer current = (Integer) map.get(PROVISIONING_UPDATE_COUNT); - Integer newValue = current + 1; - map.put(PROVISIONING_UPDATE_COUNT, newValue); - } - - @Override - public synchronized void addInformation(ZipInputStream zis) throws IOException { - throw new UnsupportedOperationException(); - } - - class Information extends Dictionary { - - @Override - public int size() { - return map.size(); - } - - @Override - public boolean isEmpty() { - return map.isEmpty(); - } - - @Override - public Enumeration keys() { - Iterator it = map.keySet().iterator(); - return new Enumeration() { - - @Override - public boolean hasMoreElements() { - return it.hasNext(); - } - - @Override - public String nextElement() { - return it.next(); - } - - }; - } - - @Override - public Enumeration elements() { - Iterator it = map.values().iterator(); - return new Enumeration() { - - @Override - public boolean hasMoreElements() { - return it.hasNext(); - } - - @Override - public Object nextElement() { - return it.next(); - } - - }; - } - - @Override - public Object get(Object key) { - return map.get(key); - } - - @Override - public Object put(String key, Object value) { - throw new UnsupportedOperationException(); - } - - @Override - public Object remove(Object key) { - throw new UnsupportedOperationException(); - } - - } -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/provisioning/package-info.java b/org.argeo.enterprise/src/org/argeo/osgi/provisioning/package-info.java deleted file mode 100644 index 1859887e2..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/provisioning/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** OSGi provisioning support. */ -package org.argeo.osgi.provisioning; \ No newline at end of file diff --git a/org.argeo.enterprise/src/org/argeo/osgi/transaction/JtaStatusAdapter.java b/org.argeo.enterprise/src/org/argeo/osgi/transaction/JtaStatusAdapter.java deleted file mode 100644 index 2dd94c654..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/transaction/JtaStatusAdapter.java +++ /dev/null @@ -1,61 +0,0 @@ -package org.argeo.osgi.transaction; - -/** JTA transaction status. */ -public class JtaStatusAdapter implements TransactionStatusAdapter { - private static final Integer STATUS_ACTIVE = 0; - private static final Integer STATUS_COMMITTED = 3; - private static final Integer STATUS_COMMITTING = 8; - private static final Integer STATUS_MARKED_ROLLBACK = 1; - private static final Integer STATUS_NO_TRANSACTION = 6; - private static final Integer STATUS_PREPARED = 2; - private static final Integer STATUS_PREPARING = 7; - private static final Integer STATUS_ROLLEDBACK = 4; - private static final Integer STATUS_ROLLING_BACK = 9; -// private static final Integer STATUS_UNKNOWN = 5; - - @Override - public Integer getActiveStatus() { - return STATUS_ACTIVE; - } - - @Override - public Integer getPreparingStatus() { - return STATUS_PREPARING; - } - - @Override - public Integer getMarkedRollbackStatus() { - return STATUS_MARKED_ROLLBACK; - } - - @Override - public Integer getPreparedStatus() { - return STATUS_PREPARED; - } - - @Override - public Integer getCommittingStatus() { - return STATUS_COMMITTING; - } - - @Override - public Integer getCommittedStatus() { - return STATUS_COMMITTED; - } - - @Override - public Integer getRollingBackStatus() { - return STATUS_ROLLING_BACK; - } - - @Override - public Integer getRolledBackStatus() { - return STATUS_ROLLEDBACK; - } - - @Override - public Integer getNoTransactionStatus() { - return STATUS_NO_TRANSACTION; - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/transaction/SimpleRollbackException.java b/org.argeo.enterprise/src/org/argeo/osgi/transaction/SimpleRollbackException.java deleted file mode 100644 index cf8a80b83..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/transaction/SimpleRollbackException.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.argeo.osgi.transaction; - -/** Internal unchecked rollback exception. */ -class SimpleRollbackException extends RuntimeException { - private static final long serialVersionUID = 8055601819719780566L; - - public SimpleRollbackException() { - super(); - } - - public SimpleRollbackException(Throwable cause) { - super(cause); - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/transaction/SimpleTransaction.java b/org.argeo.enterprise/src/org/argeo/osgi/transaction/SimpleTransaction.java deleted file mode 100644 index e668b317e..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/transaction/SimpleTransaction.java +++ /dev/null @@ -1,160 +0,0 @@ -package org.argeo.osgi.transaction; - -import java.util.ArrayList; -import java.util.List; - -import javax.transaction.xa.XAException; -import javax.transaction.xa.XAResource; -import javax.transaction.xa.Xid; - -/** Simple implementation of an XA transaction. */ -class SimpleTransaction -//implements Transaction, Status -{ - private final Xid xid; - private T status; - private final List xaResources = new ArrayList(); - - private final SimpleTransactionManager transactionManager; - private TransactionStatusAdapter tsa; - - public SimpleTransaction(SimpleTransactionManager transactionManager, TransactionStatusAdapter tsa) { - this.tsa = tsa; - this.status = tsa.getActiveStatus(); - this.xid = new UuidXid(); - this.transactionManager = transactionManager; - } - - public synchronized void commit() -// throws RollbackException, HeuristicMixedException, HeuristicRollbackException, -// SecurityException, IllegalStateException, SystemException - { - status = tsa.getPreparingStatus(); - for (XAResource xaRes : xaResources) { - if (status.equals(tsa.getMarkedRollbackStatus())) - break; - try { - xaRes.prepare(xid); - } catch (XAException e) { - status = tsa.getMarkedRollbackStatus(); - error("Cannot prepare " + xaRes + " for " + xid, e); - } - } - if (status.equals(tsa.getMarkedRollbackStatus())) { - rollback(); - throw new SimpleRollbackException(); - } - status = tsa.getPreparedStatus(); - - status = tsa.getCommittingStatus(); - for (XAResource xaRes : xaResources) { - if (status.equals(tsa.getMarkedRollbackStatus())) - break; - try { - xaRes.commit(xid, false); - } catch (XAException e) { - status = tsa.getMarkedRollbackStatus(); - error("Cannot prepare " + xaRes + " for " + xid, e); - } - } - if (status.equals(tsa.getMarkedRollbackStatus())) { - rollback(); - throw new SimpleRollbackException(); - } - - // complete - status = tsa.getCommittedStatus(); - clearResources(XAResource.TMSUCCESS); - transactionManager.unregister(xid); - } - - public synchronized void rollback() -// throws IllegalStateException, SystemException - { - status = tsa.getRollingBackStatus(); - for (XAResource xaRes : xaResources) { - try { - xaRes.rollback(xid); - } catch (XAException e) { - error("Cannot rollback " + xaRes + " for " + xid, e); - } - } - - // complete - status = tsa.getRolledBackStatus(); - clearResources(XAResource.TMFAIL); - transactionManager.unregister(xid); - } - - public synchronized boolean enlistResource(XAResource xaRes) -// throws RollbackException, IllegalStateException, SystemException - { - if (xaResources.add(xaRes)) { - try { - xaRes.start(getXid(), XAResource.TMNOFLAGS); - return true; - } catch (XAException e) { - error("Cannot enlist " + xaRes, e); - return false; - } - } else - return false; - } - - public synchronized boolean delistResource(XAResource xaRes, int flag) -// throws IllegalStateException, SystemException - { - if (xaResources.remove(xaRes)) { - try { - xaRes.end(getXid(), flag); - } catch (XAException e) { - error("Cannot delist " + xaRes, e); - return false; - } - return true; - } else - return false; - } - - protected void clearResources(int flag) { - for (XAResource xaRes : xaResources) - try { - xaRes.end(getXid(), flag); - } catch (XAException e) { - error("Cannot end " + xaRes, e); - } - xaResources.clear(); - } - - protected void error(Object obj, Exception e) { - System.err.println(obj); - e.printStackTrace(); - } - - public synchronized T getStatus() -// throws SystemException - { - return status; - } - -// public void registerSynchronization(Synchronization sync) -// throws RollbackException, IllegalStateException, SystemException { -// throw new UnsupportedOperationException(); -// } - - public void setRollbackOnly() -// throws IllegalStateException, SystemException - { - status = tsa.getMarkedRollbackStatus(); - } - - @Override - public int hashCode() { - return xid.hashCode(); - } - - Xid getXid() { - return xid; - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/transaction/SimpleTransactionManager.java b/org.argeo.enterprise/src/org/argeo/osgi/transaction/SimpleTransactionManager.java deleted file mode 100644 index 3d4edfd17..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/transaction/SimpleTransactionManager.java +++ /dev/null @@ -1,214 +0,0 @@ -package org.argeo.osgi.transaction; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.Callable; - -import javax.transaction.xa.XAResource; -import javax.transaction.xa.Xid; - -/** - * Simple implementation of an XA transaction manager. - */ -public class SimpleTransactionManager -// implements TransactionManager, UserTransaction - implements WorkControl, WorkTransaction { - private ThreadLocal> current = new ThreadLocal>(); - - private Map> knownTransactions = Collections - .synchronizedMap(new HashMap>()); - private TransactionStatusAdapter tsa = new JtaStatusAdapter(); -// private SyncRegistry syncRegistry = new SyncRegistry(); - - /* - * WORK IMPLEMENTATION - */ - @Override - public T required(Callable work) { - T res; - begin(); - try { - res = work.call(); - commit(); - } catch (Exception e) { - rollback(); - throw new SimpleRollbackException(e); - } - return res; - } - - @Override - public WorkContext getWorkContext() { - return new WorkContext() { - - @Override - public void registerXAResource(XAResource resource, String recoveryId) { - getTransaction().enlistResource(resource); - } - }; - } - - /* - * WORK TRANSACTION IMPLEMENTATION - */ - - @Override - public boolean isNoTransactionStatus() { - return tsa.getNoTransactionStatus().equals(getStatus()); - } - - /* - * JTA IMPLEMENTATION - */ - - public void begin() -// throws NotSupportedException, SystemException - { - if (getCurrent() != null) - throw new UnsupportedOperationException("Nested transactions are not supported"); - SimpleTransaction transaction = new SimpleTransaction(this, tsa); - knownTransactions.put(transaction.getXid(), transaction); - current.set(transaction); - } - - public void commit() -// throws RollbackException, HeuristicMixedException, HeuristicRollbackException, -// SecurityException, IllegalStateException, SystemException - { - if (getCurrent() == null) - throw new IllegalStateException("No transaction registered with the current thread."); - getCurrent().commit(); - } - - public int getStatus() -// throws SystemException - { - if (getCurrent() == null) - return tsa.getNoTransactionStatus(); - return getTransaction().getStatus(); - } - - public SimpleTransaction getTransaction() -// throws SystemException - { - return getCurrent(); - } - - protected SimpleTransaction getCurrent() -// throws SystemException - { - SimpleTransaction transaction = current.get(); - if (transaction == null) - return null; - Integer status = transaction.getStatus(); - if (status.equals(tsa.getCommittedStatus()) || status.equals(tsa.getRolledBackStatus())) { - current.remove(); - return null; - } - return transaction; - } - - void unregister(Xid xid) { - knownTransactions.remove(xid); - } - - public void resume(SimpleTransaction tobj) -// throws InvalidTransactionException, IllegalStateException, SystemException - { - if (getCurrent() != null) - throw new IllegalStateException("Transaction " + current.get() + " already registered"); - current.set(tobj); - } - - public void rollback() -// throws IllegalStateException, SecurityException, SystemException - { - if (getCurrent() == null) - throw new IllegalStateException("No transaction registered with the current thread."); - getCurrent().rollback(); - } - - public void setRollbackOnly() -// throws IllegalStateException, SystemException - { - if (getCurrent() == null) - throw new IllegalStateException("No transaction registered with the current thread."); - getCurrent().setRollbackOnly(); - } - - public void setTransactionTimeout(int seconds) -// throws SystemException - { - throw new UnsupportedOperationException(); - } - - public SimpleTransaction suspend() -// throws SystemException - { - SimpleTransaction transaction = getCurrent(); - current.remove(); - return transaction; - } - -// public TransactionSynchronizationRegistry getTsr() { -// return syncRegistry; -// } -// -// private class SyncRegistry implements TransactionSynchronizationRegistry { -// @Override -// public Object getTransactionKey() { -// try { -// SimpleTransaction transaction = getCurrent(); -// if (transaction == null) -// return null; -// return getCurrent().getXid(); -// } catch (SystemException e) { -// throw new IllegalStateException("Cannot get transaction key", e); -// } -// } -// -// @Override -// public void putResource(Object key, Object value) { -// throw new UnsupportedOperationException(); -// } -// -// @Override -// public Object getResource(Object key) { -// throw new UnsupportedOperationException(); -// } -// -// @Override -// public void registerInterposedSynchronization(Synchronization sync) { -// throw new UnsupportedOperationException(); -// } -// -// @Override -// public int getTransactionStatus() { -// try { -// return getStatus(); -// } catch (SystemException e) { -// throw new IllegalStateException("Cannot get status", e); -// } -// } -// -// @Override -// public boolean getRollbackOnly() { -// try { -// return getStatus() == Status.STATUS_MARKED_ROLLBACK; -// } catch (SystemException e) { -// throw new IllegalStateException("Cannot get status", e); -// } -// } -// -// @Override -// public void setRollbackOnly() { -// try { -// getCurrent().setRollbackOnly(); -// } catch (Exception e) { -// throw new IllegalStateException("Cannot set rollback only", e); -// } -// } -// -// } -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/transaction/TransactionStatusAdapter.java b/org.argeo.enterprise/src/org/argeo/osgi/transaction/TransactionStatusAdapter.java deleted file mode 100644 index 87abceba4..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/transaction/TransactionStatusAdapter.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.argeo.osgi.transaction; - -/** Abstract the various approaches to represent transaction status. */ -public interface TransactionStatusAdapter { - T getActiveStatus(); - - T getPreparingStatus(); - - T getMarkedRollbackStatus(); - - T getPreparedStatus(); - - T getCommittingStatus(); - - T getCommittedStatus(); - - T getRollingBackStatus(); - - T getRolledBackStatus(); - - T getNoTransactionStatus(); -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/transaction/UuidXid.java b/org.argeo.enterprise/src/org/argeo/osgi/transaction/UuidXid.java deleted file mode 100644 index 729aef8e5..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/transaction/UuidXid.java +++ /dev/null @@ -1,132 +0,0 @@ -package org.argeo.osgi.transaction; - -import java.io.Serializable; -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.UUID; - -import javax.transaction.xa.Xid; - -/** - * Implementation of {@link Xid} based on {@link UUID}, using max significant - * bits as global transaction id, and least significant bits as branch - * qualifier. - */ -public class UuidXid implements Xid, Serializable { - private static final long serialVersionUID = -5380531989917886819L; - public final static int FORMAT = (int) serialVersionUID; - - private final static int BYTES_PER_LONG = Long.SIZE / Byte.SIZE; - - private final int format; - private final byte[] globalTransactionId; - private final byte[] branchQualifier; - private final String uuid; - private final int hashCode; - - public UuidXid() { - this(UUID.randomUUID()); - } - - public UuidXid(UUID uuid) { - this.format = FORMAT; - this.globalTransactionId = uuidToBytes(uuid.getMostSignificantBits()); - this.branchQualifier = uuidToBytes(uuid.getLeastSignificantBits()); - this.uuid = uuid.toString(); - this.hashCode = uuid.hashCode(); - } - - public UuidXid(Xid xid) { - this(xid.getFormatId(), xid.getGlobalTransactionId(), xid - .getBranchQualifier()); - } - - private UuidXid(int format, byte[] globalTransactionId, - byte[] branchQualifier) { - this.format = format; - this.globalTransactionId = globalTransactionId; - this.branchQualifier = branchQualifier; - this.uuid = bytesToUUID(globalTransactionId, branchQualifier) - .toString(); - this.hashCode = uuid.hashCode(); - } - - @Override - public int getFormatId() { - return format; - } - - @Override - public byte[] getGlobalTransactionId() { - return Arrays.copyOf(globalTransactionId, globalTransactionId.length); - } - - @Override - public byte[] getBranchQualifier() { - return Arrays.copyOf(branchQualifier, branchQualifier.length); - } - - @Override - public int hashCode() { - return hashCode; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj instanceof UuidXid) { - UuidXid that = (UuidXid) obj; - return Arrays.equals(globalTransactionId, that.globalTransactionId) - && Arrays.equals(branchQualifier, that.branchQualifier); - } - if (obj instanceof Xid) { - Xid that = (Xid) obj; - return Arrays.equals(globalTransactionId, - that.getGlobalTransactionId()) - && Arrays - .equals(branchQualifier, that.getBranchQualifier()); - } - return uuid.equals(obj.toString()); - } - - @Override - protected Object clone() throws CloneNotSupportedException { - return new UuidXid(format, globalTransactionId, branchQualifier); - } - - @Override - public String toString() { - return uuid; - } - - public UUID asUuid() { - return bytesToUUID(globalTransactionId, branchQualifier); - } - - public static byte[] uuidToBytes(long bits) { - ByteBuffer buffer = ByteBuffer.allocate(BYTES_PER_LONG); - buffer.putLong(0, bits); - return buffer.array(); - } - - public static UUID bytesToUUID(byte[] most, byte[] least) { - if (most.length < BYTES_PER_LONG) - most = Arrays.copyOf(most, BYTES_PER_LONG); - if (least.length < BYTES_PER_LONG) - least = Arrays.copyOf(least, BYTES_PER_LONG); - ByteBuffer buffer = ByteBuffer.allocate(2 * BYTES_PER_LONG); - buffer.put(most, 0, BYTES_PER_LONG); - buffer.put(least, 0, BYTES_PER_LONG); - buffer.flip(); - return new UUID(buffer.getLong(), buffer.getLong()); - } - - // public static void main(String[] args) { - // UUID uuid = UUID.randomUUID(); - // System.out.println(uuid); - // uuid = bytesToUUID(uuidToBytes(uuid.getMostSignificantBits()), - // uuidToBytes(uuid.getLeastSignificantBits())); - // System.out.println(uuid); - // } -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/transaction/WorkContext.java b/org.argeo.enterprise/src/org/argeo/osgi/transaction/WorkContext.java deleted file mode 100644 index f50f20870..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/transaction/WorkContext.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.argeo.osgi.transaction; - -import javax.transaction.xa.XAResource; - -/** - * A minimalistic interface similar to OSGi transaction context in order to - * register XA resources. - */ -public interface WorkContext { - void registerXAResource(XAResource resource, String recoveryId); -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/transaction/WorkControl.java b/org.argeo.enterprise/src/org/argeo/osgi/transaction/WorkControl.java deleted file mode 100644 index 766809555..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/transaction/WorkControl.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.argeo.osgi.transaction; - -import java.util.concurrent.Callable; - -/** - * A minimalistic interface inspired by OSGi transaction control in order to - * commit units of work externally. - */ -public interface WorkControl { - T required(Callable work); - - void setRollbackOnly(); - - WorkContext getWorkContext(); -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/transaction/WorkTransaction.java b/org.argeo.enterprise/src/org/argeo/osgi/transaction/WorkTransaction.java deleted file mode 100644 index 653390912..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/transaction/WorkTransaction.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.argeo.osgi.transaction; - -/** - * A minimalistic interface inspired by JTA user transaction in order to commit - * units of work externally. - */ -public interface WorkTransaction { - void begin(); - - void commit(); - - void rollback(); - - boolean isNoTransactionStatus(); -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/transaction/package-info.java b/org.argeo.enterprise/src/org/argeo/osgi/transaction/package-info.java deleted file mode 100644 index 3d3756243..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/transaction/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Minimalistic and partial XA transaction manager implementation. */ -package org.argeo.osgi.transaction; \ No newline at end of file diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java deleted file mode 100644 index 7279877e0..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java +++ /dev/null @@ -1,509 +0,0 @@ -package org.argeo.osgi.useradmin; - -import static org.argeo.naming.LdapAttrs.objectClass; -import static org.argeo.naming.LdapObjs.extensibleObject; -import static org.argeo.naming.LdapObjs.inetOrgPerson; -import static org.argeo.naming.LdapObjs.organizationalPerson; -import static org.argeo.naming.LdapObjs.person; -import static org.argeo.naming.LdapObjs.top; - -import java.io.File; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Dictionary; -import java.util.Enumeration; -import java.util.Hashtable; -import java.util.Iterator; -import java.util.List; - -import javax.naming.InvalidNameException; -import javax.naming.NameNotFoundException; -import javax.naming.NamingEnumeration; -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.naming.LdapAttrs; -import org.argeo.osgi.transaction.WorkControl; -import org.osgi.framework.Filter; -import org.osgi.framework.FrameworkUtil; -import org.osgi.framework.InvalidSyntaxException; -import org.osgi.service.useradmin.Authorization; -import org.osgi.service.useradmin.Role; -import org.osgi.service.useradmin.User; -import org.osgi.service.useradmin.UserAdmin; - -/** Base class for a {@link UserDirectory}. */ -public abstract class AbstractUserDirectory implements UserAdmin, UserDirectory { - static final String SHARED_STATE_USERNAME = "javax.security.auth.login.name"; - static final String SHARED_STATE_PASSWORD = "javax.security.auth.login.password"; - - private final Hashtable properties; - private final LdapName baseDn, userBaseDn, groupBaseDn; - private final String userObjectClass, userBase, groupObjectClass, groupBase; - - private final boolean readOnly; - private final boolean disabled; - private final String uri; - - private UserAdmin externalRoles; - // private List indexedUserProperties = Arrays - // .asList(new String[] { LdapAttrs.uid.name(), LdapAttrs.mail.name(), - // LdapAttrs.cn.name() }); - - private final boolean scoped; - - private String memberAttributeId = "member"; - private List credentialAttributeIds = Arrays - .asList(new String[] { LdapAttrs.userPassword.name(), LdapAttrs.authPassword.name() }); - - // Transaction -// private TransactionManager transactionManager; - private WorkControl transactionControl; - private WcXaResource xaResource = new WcXaResource(this); - - AbstractUserDirectory(URI uriArg, Dictionary props, boolean scoped) { - this.scoped = scoped; - properties = new Hashtable(); - for (Enumeration keys = props.keys(); keys.hasMoreElements();) { - String key = keys.nextElement(); - properties.put(key, props.get(key)); - } - - if (uriArg != null) { - uri = uriArg.toString(); - // uri from properties is ignored - } else { - String uriStr = UserAdminConf.uri.getValue(properties); - if (uriStr == null) - uri = null; - else - uri = uriStr; - } - - userObjectClass = UserAdminConf.userObjectClass.getValue(properties); - userBase = UserAdminConf.userBase.getValue(properties); - groupObjectClass = UserAdminConf.groupObjectClass.getValue(properties); - groupBase = UserAdminConf.groupBase.getValue(properties); - try { - baseDn = new LdapName(UserAdminConf.baseDn.getValue(properties)); - userBaseDn = new LdapName(userBase + "," + baseDn); - groupBaseDn = new LdapName(groupBase + "," + baseDn); - } catch (InvalidNameException e) { - throw new UserDirectoryException("Badly formated base DN " + UserAdminConf.baseDn.getValue(properties), e); - } - String readOnlyStr = UserAdminConf.readOnly.getValue(properties); - if (readOnlyStr == null) { - readOnly = readOnlyDefault(uri); - properties.put(UserAdminConf.readOnly.name(), Boolean.toString(readOnly)); - } else - readOnly = Boolean.parseBoolean(readOnlyStr); - String disabledStr = UserAdminConf.disabled.getValue(properties); - if (disabledStr != null) - disabled = Boolean.parseBoolean(disabledStr); - else - disabled = false; - } - - /** Returns the groups this user is a direct member of. */ - protected abstract List getDirectGroups(LdapName dn); - - protected abstract Boolean daoHasRole(LdapName dn); - - protected abstract DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException; - - protected abstract List doGetRoles(Filter f); - - protected abstract AbstractUserDirectory scope(User user); - - public void init() { - - } - - public void destroy() { - - } - - protected boolean isEditing() { - return xaResource.wc() != null; - } - - protected UserDirectoryWorkingCopy getWorkingCopy() { - UserDirectoryWorkingCopy wc = xaResource.wc(); - if (wc == null) - return null; - return wc; - } - - protected void checkEdit() { -// Transaction transaction; -// try { -// transaction = transactionManager.getTransaction(); -// } catch (SystemException e) { -// throw new UserDirectoryException("Cannot get transaction", e); -// } -// if (transaction == null) -// throw new UserDirectoryException("A transaction needs to be active in order to edit"); - if (xaResource.wc() == null) { - try { -// transaction.enlistResource(xaResource); - transactionControl.getWorkContext().registerXAResource(xaResource, null); - } catch (Exception e) { - throw new UserDirectoryException("Cannot enlist " + xaResource, e); - } - } else { - } - } - - protected List getAllRoles(DirectoryUser user) { - List allRoles = new ArrayList(); - if (user != null) { - collectRoles(user, allRoles); - allRoles.add(user); - } else - collectAnonymousRoles(allRoles); - return allRoles; - } - - private void collectRoles(DirectoryUser user, List allRoles) { - Attributes attrs = user.getAttributes(); - // TODO centralize attribute name - Attribute memberOf = attrs.get(LdapAttrs.memberOf.name()); - // if user belongs to this directory, we only check meberOf - if (memberOf != null && user.getDn().startsWith(getBaseDn())) { - try { - NamingEnumeration values = memberOf.getAll(); - while (values.hasMore()) { - Object value = values.next(); - LdapName groupDn = new LdapName(value.toString()); - DirectoryUser group = doGetRole(groupDn); - if (group != null) - allRoles.add(group); - } - } catch (Exception e) { - throw new UserDirectoryException("Cannot get memberOf groups for " + user, e); - } - } else { - for (LdapName groupDn : getDirectGroups(user.getDn())) { - // TODO check for loops - DirectoryUser group = doGetRole(groupDn); - if (group != null) { - allRoles.add(group); - collectRoles(group, allRoles); - } - } - } - } - - private void collectAnonymousRoles(List allRoles) { - // TODO gather anonymous roles - } - - // USER ADMIN - @Override - public Role getRole(String name) { - return doGetRole(toDn(name)); - } - - protected DirectoryUser doGetRole(LdapName dn) { - UserDirectoryWorkingCopy wc = getWorkingCopy(); - DirectoryUser user; - try { - user = daoGetRole(dn); - } catch (NameNotFoundException e) { - user = null; - } - if (wc != null) { - if (user == null && wc.getNewUsers().containsKey(dn)) - user = wc.getNewUsers().get(dn); - else if (wc.getDeletedUsers().containsKey(dn)) - user = null; - } - return user; - } - - @Override - public Role[] getRoles(String filter) throws InvalidSyntaxException { - UserDirectoryWorkingCopy wc = getWorkingCopy(); - Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null; - List res = doGetRoles(f); - if (wc != null) { - for (Iterator it = res.iterator(); it.hasNext();) { - DirectoryUser user = it.next(); - LdapName dn = user.getDn(); - if (wc.getDeletedUsers().containsKey(dn)) - it.remove(); - } - for (DirectoryUser user : wc.getNewUsers().values()) { - if (f == null || f.match(user.getProperties())) - res.add(user); - } - // no need to check modified users, - // since doGetRoles was already based on the modified attributes - } - return res.toArray(new Role[res.size()]); - } - - @Override - public User getUser(String key, String value) { - // TODO check value null or empty - List collectedUsers = new ArrayList(); - if (key != null) { - doGetUser(key, value, collectedUsers); - } else { - throw new UserDirectoryException("Key cannot be null"); - } - - if (collectedUsers.size() == 1) { - return collectedUsers.get(0); - } else if (collectedUsers.size() > 1) { - // log.warn(collectedUsers.size() + " users for " + (key != null ? key + "=" : - // "") + value); - } - return null; - } - - protected void doGetUser(String key, String value, List collectedUsers) { - try { - Filter f = FrameworkUtil.createFilter("(" + key + "=" + value + ")"); - List users = doGetRoles(f); - collectedUsers.addAll(users); - } catch (InvalidSyntaxException e) { - throw new UserDirectoryException("Cannot get user with " + key + "=" + value, e); - } - } - - @Override - public Authorization getAuthorization(User user) { - if (user == null || user instanceof DirectoryUser) { - return new LdifAuthorization(user, getAllRoles((DirectoryUser) user)); - } else { - // bind - AbstractUserDirectory scopedUserAdmin = scope(user); - try { - DirectoryUser directoryUser = (DirectoryUser) scopedUserAdmin.getRole(user.getName()); - if (directoryUser == null) - throw new UserDirectoryException("No scoped user found for " + user); - LdifAuthorization authorization = new LdifAuthorization(directoryUser, - scopedUserAdmin.getAllRoles(directoryUser)); - return authorization; - } finally { - scopedUserAdmin.destroy(); - } - } - } - - @Override - public Role createRole(String name, int type) { - checkEdit(); - UserDirectoryWorkingCopy wc = getWorkingCopy(); - LdapName dn = toDn(name); - if ((daoHasRole(dn) && !wc.getDeletedUsers().containsKey(dn)) || wc.getNewUsers().containsKey(dn)) - throw new UserDirectoryException("Already a role " + name); - BasicAttributes attrs = new BasicAttributes(true); - // attrs.put(LdifName.dn.name(), dn.toString()); - Rdn nameRdn = dn.getRdn(dn.size() - 1); - // TODO deal with multiple attr RDN - attrs.put(nameRdn.getType(), nameRdn.getValue()); - if (wc.getDeletedUsers().containsKey(dn)) { - wc.getDeletedUsers().remove(dn); - wc.getModifiedUsers().put(dn, attrs); - return getRole(name); - } else { - wc.getModifiedUsers().put(dn, attrs); - DirectoryUser newRole = newRole(dn, type, attrs); - wc.getNewUsers().put(dn, newRole); - return newRole; - } - } - - protected DirectoryUser newRole(LdapName dn, int type, Attributes attrs) { - LdifUser newRole; - BasicAttribute objClass = new BasicAttribute(objectClass.name()); - if (type == Role.USER) { - String userObjClass = newUserObjectClass(dn); - objClass.add(userObjClass); - if (inetOrgPerson.name().equals(userObjClass)) { - objClass.add(organizationalPerson.name()); - objClass.add(person.name()); - } else if (organizationalPerson.name().equals(userObjClass)) { - objClass.add(person.name()); - } - objClass.add(top.name()); - objClass.add(extensibleObject.name()); - attrs.put(objClass); - newRole = new LdifUser(this, dn, attrs); - } else if (type == Role.GROUP) { - String groupObjClass = getGroupObjectClass(); - objClass.add(groupObjClass); - // objClass.add(LdifName.extensibleObject.name()); - objClass.add(top.name()); - attrs.put(objClass); - newRole = new LdifGroup(this, dn, attrs); - } else - throw new UserDirectoryException("Unsupported type " + type); - return newRole; - } - - @Override - public boolean removeRole(String name) { - checkEdit(); - UserDirectoryWorkingCopy wc = getWorkingCopy(); - LdapName dn = toDn(name); - boolean actuallyDeleted; - if (daoHasRole(dn) || wc.getNewUsers().containsKey(dn)) { - DirectoryUser user = (DirectoryUser) getRole(name); - wc.getDeletedUsers().put(dn, user); - actuallyDeleted = true; - } else {// just removing from groups (e.g. system roles) - actuallyDeleted = false; - } - for (LdapName groupDn : getDirectGroups(dn)) { - DirectoryUser group = doGetRole(groupDn); - group.getAttributes().get(getMemberAttributeId()).remove(dn.toString()); - } - return actuallyDeleted; - } - - // TRANSACTION - protected void prepare(UserDirectoryWorkingCopy wc) { - - } - - protected void commit(UserDirectoryWorkingCopy wc) { - - } - - protected void rollback(UserDirectoryWorkingCopy wc) { - - } - - // UTILITIES - protected LdapName toDn(String name) { - try { - return new LdapName(name); - } catch (InvalidNameException e) { - throw new UserDirectoryException("Badly formatted name", e); - } - } - - // GETTERS - protected String getMemberAttributeId() { - return memberAttributeId; - } - - protected List getCredentialAttributeIds() { - return credentialAttributeIds; - } - - protected String getUri() { - return uri; - } - - private static boolean readOnlyDefault(String uriStr) { - if (uriStr == null) - return true; - /// TODO make it more generic - URI uri; - try { - uri = new URI(uriStr.split(" ")[0]); - } catch (URISyntaxException e) { - throw new IllegalArgumentException(e); - } - if (uri.getScheme() == null) - return false;// assume relative file to be writable - if (uri.getScheme().equals(UserAdminConf.SCHEME_FILE)) { - File file = new File(uri); - if (file.exists()) - return !file.canWrite(); - else - return !file.getParentFile().canWrite(); - } else if (uri.getScheme().equals(UserAdminConf.SCHEME_LDAP)) { - if (uri.getAuthority() != null)// assume writable if authenticated - return false; - } else if (uri.getScheme().equals(UserAdminConf.SCHEME_OS)) { - return true; - } - return true;// read only by default - } - - public boolean isReadOnly() { - return readOnly; - } - - public boolean isDisabled() { - return disabled; - } - - protected UserAdmin getExternalRoles() { - return externalRoles; - } - - protected int roleType(LdapName dn) { - if (dn.startsWith(groupBaseDn)) - return Role.GROUP; - else if (dn.startsWith(userBaseDn)) - return Role.USER; - else - return Role.GROUP; - } - - /** dn can be null, in that case a default should be returned. */ - public String getUserObjectClass() { - return userObjectClass; - } - - public String getUserBase() { - return userBase; - } - - protected String newUserObjectClass(LdapName dn) { - return getUserObjectClass(); - } - - public String getGroupObjectClass() { - return groupObjectClass; - } - - public String getGroupBase() { - return groupBase; - } - - public LdapName getBaseDn() { - return (LdapName) baseDn.clone(); - } - - public Dictionary getProperties() { - return properties; - } - - public Dictionary cloneProperties() { - return new Hashtable<>(properties); - } - - public void setExternalRoles(UserAdmin externalRoles) { - this.externalRoles = externalRoles; - } - -// public void setTransactionManager(TransactionManager transactionManager) { -// this.transactionManager = transactionManager; -// } - - public void setTransactionControl(WorkControl transactionControl) { - this.transactionControl = transactionControl; - } - - public WcXaResource getXaResource() { - return xaResource; - } - - public boolean isScoped() { - return scoped; - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AggregatingAuthorization.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AggregatingAuthorization.java deleted file mode 100644 index 05ba94889..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AggregatingAuthorization.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import javax.security.auth.x500.X500Principal; - -import org.osgi.service.useradmin.Authorization; - -/** An {@link Authorization} which combines roles form various auth sources. */ -class AggregatingAuthorization implements Authorization { - private final String name; - private final String displayName; - private final Set systemRoles; - private final Set roles; - - public AggregatingAuthorization(String name, String displayName, Set systemRoles, String[] roles) { - this.name = new X500Principal(name).getName(); - this.displayName = displayName; - this.systemRoles = Collections.unmodifiableSet(new HashSet<>(systemRoles)); - Set temp = new HashSet<>(); - for (String role : roles) { - if (!temp.contains(role)) - temp.add(role); - } - this.roles = Collections.unmodifiableSet(temp); - } - - @Override - public String getName() { - return name; - } - - @Override - public boolean hasRole(String name) { - if (systemRoles.contains(name)) - return true; - if (roles.contains(name)) - return true; - return false; - } - - @Override - public String[] getRoles() { - int size = systemRoles.size() + roles.size(); - List res = new ArrayList(size); - res.addAll(systemRoles); - res.addAll(roles); - return res.toArray(new String[size]); - } - - @Override - public int hashCode() { - if (name == null) - return super.hashCode(); - return name.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof Authorization)) - return false; - Authorization that = (Authorization) obj; - if (name == null) - return that.getName() == null; - return name.equals(that.getName()); - } - - @Override - public String toString() { - return displayName; - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java deleted file mode 100644 index bee513546..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java +++ /dev/null @@ -1,276 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; - -import org.osgi.framework.InvalidSyntaxException; -import org.osgi.service.useradmin.Authorization; -import org.osgi.service.useradmin.Group; -import org.osgi.service.useradmin.Role; -import org.osgi.service.useradmin.User; -import org.osgi.service.useradmin.UserAdmin; - -/** - * Aggregates multiple {@link UserDirectory} and integrates them with system - * roles. - */ -public class AggregatingUserAdmin implements UserAdmin { - private final LdapName systemRolesBaseDn; - private final LdapName tokensBaseDn; - - // DAOs - private AbstractUserDirectory systemRoles = null; - private AbstractUserDirectory tokens = null; - private Map businessRoles = new HashMap(); - - public AggregatingUserAdmin(String systemRolesBaseDn, String tokensBaseDn) { - try { - this.systemRolesBaseDn = new LdapName(systemRolesBaseDn); - if (tokensBaseDn != null) - this.tokensBaseDn = new LdapName(tokensBaseDn); - else - this.tokensBaseDn = null; - } catch (InvalidNameException e) { - throw new UserDirectoryException("Cannot initialize " + AggregatingUserAdmin.class, e); - } - } - - @Override - public Role createRole(String name, int type) { - return findUserAdmin(name).createRole(name, type); - } - - @Override - public boolean removeRole(String name) { - boolean actuallyDeleted = findUserAdmin(name).removeRole(name); - systemRoles.removeRole(name); - return actuallyDeleted; - } - - @Override - public Role getRole(String name) { - return findUserAdmin(name).getRole(name); - } - - @Override - public Role[] getRoles(String filter) throws InvalidSyntaxException { - List res = new ArrayList(); - for (UserAdmin userAdmin : businessRoles.values()) { - res.addAll(Arrays.asList(userAdmin.getRoles(filter))); - } - res.addAll(Arrays.asList(systemRoles.getRoles(filter))); - return res.toArray(new Role[res.size()]); - } - - @Override - public User getUser(String key, String value) { - List res = new ArrayList(); - for (UserAdmin userAdmin : businessRoles.values()) { - User u = userAdmin.getUser(key, value); - if (u != null) - res.add(u); - } - // Note: node roles cannot contain users, so it is not searched - return res.size() == 1 ? res.get(0) : null; - } - - @Override - public Authorization getAuthorization(User user) { - if (user == null) {// anonymous - return systemRoles.getAuthorization(null); - } - AbstractUserDirectory userReferentialOfThisUser = findUserAdmin(user.getName()); - Authorization rawAuthorization = userReferentialOfThisUser.getAuthorization(user); - String usernameToUse; - String displayNameToUse; - if (user instanceof Group) { - // TODO check whether this is still working - String ownerDn = TokenUtils.userDn((Group) user); - if (ownerDn != null) {// tokens - UserAdmin ownerUserAdmin = findUserAdmin(ownerDn); - User ownerUser = (User) ownerUserAdmin.getRole(ownerDn); - usernameToUse = ownerDn; - displayNameToUse = LdifAuthorization.extractDisplayName(ownerUser); - } else { - usernameToUse = rawAuthorization.getName(); - displayNameToUse = rawAuthorization.toString(); - } - } else {// regular users - usernameToUse = rawAuthorization.getName(); - displayNameToUse = rawAuthorization.toString(); - } - - // gather roles from other referentials - final AbstractUserDirectory userAdminToUse;// possibly scoped when authenticating - if (user instanceof DirectoryUser) { - userAdminToUse = userReferentialOfThisUser; - } else if (user instanceof AuthenticatingUser) { - userAdminToUse = userReferentialOfThisUser.scope(user); - } else { - throw new IllegalArgumentException("Unsupported user type " + user.getClass()); - } - - try { - Set sysRoles = new HashSet(); - for (String role : rawAuthorization.getRoles()) { - User userOrGroup = (User) userAdminToUse.getRole(role); - Authorization auth = systemRoles.getAuthorization(userOrGroup); - systemRoles: for (String systemRole : auth.getRoles()) { - if (role.equals(systemRole)) - continue systemRoles; - sysRoles.add(systemRole); - } -// sysRoles.addAll(Arrays.asList(auth.getRoles())); - } - addAbstractSystemRoles(rawAuthorization, sysRoles); - Authorization authorization = new AggregatingAuthorization(usernameToUse, displayNameToUse, sysRoles, - rawAuthorization.getRoles()); - return authorization; - } finally { - if (userAdminToUse != null && userAdminToUse.isScoped()) { - userAdminToUse.destroy(); - } - } - } - - /** - * Enrich with application-specific roles which are strictly programmatic, such - * as anonymous/user semantics. - */ - protected void addAbstractSystemRoles(Authorization rawAuthorization, Set sysRoles) { - - } - - // - // USER ADMIN AGGREGATOR - // - protected void addUserDirectory(AbstractUserDirectory userDirectory) { - LdapName baseDn = userDirectory.getBaseDn(); - if (isSystemRolesBaseDn(baseDn)) { - this.systemRoles = userDirectory; - systemRoles.setExternalRoles(this); - } else if (isTokensBaseDn(baseDn)) { - this.tokens = userDirectory; - tokens.setExternalRoles(this); - } else { - if (businessRoles.containsKey(baseDn)) - throw new UserDirectoryException("There is already a user admin for " + baseDn); - businessRoles.put(baseDn, userDirectory); - } - userDirectory.init(); - postAdd(userDirectory); - } - - /** Called after a new user directory has been added */ - protected void postAdd(AbstractUserDirectory userDirectory) { - } - -// private UserAdmin findUserAdmin(User user) { -// if (user == null) -// throw new IllegalArgumentException("User should not be null"); -// AbstractUserDirectory userAdmin = findUserAdmin(user.getName()); -// if (user instanceof DirectoryUser) { -// return userAdmin; -// } else { -// return userAdmin.scope(user); -// } -// } - - private AbstractUserDirectory findUserAdmin(String name) { - try { - return findUserAdmin(new LdapName(name)); - } catch (InvalidNameException e) { - throw new UserDirectoryException("Badly formatted name " + name, e); - } - } - - private AbstractUserDirectory findUserAdmin(LdapName name) { - if (name.startsWith(systemRolesBaseDn)) - return systemRoles; - if (tokensBaseDn != null && name.startsWith(tokensBaseDn)) - return tokens; - List res = new ArrayList<>(1); - userDirectories: for (LdapName baseDn : businessRoles.keySet()) { - AbstractUserDirectory userDirectory = businessRoles.get(baseDn); - if (name.startsWith(baseDn)) { - if (userDirectory.isDisabled()) - continue userDirectories; -// if (res.isEmpty()) { - res.add(userDirectory); -// } else { -// for (AbstractUserDirectory ud : res) { -// LdapName bd = ud.getBaseDn(); -// if (userDirectory.getBaseDn().startsWith(bd)) { -// // child user directory -// } -// } -// } - } - } - if (res.size() == 0) - throw new UserDirectoryException("Cannot find user admin for " + name); - if (res.size() > 1) - throw new UserDirectoryException("Multiple user admin found for " + name); - return res.get(0); - } - - protected boolean isSystemRolesBaseDn(LdapName baseDn) { - return baseDn.equals(systemRolesBaseDn); - } - - protected boolean isTokensBaseDn(LdapName baseDn) { - return tokensBaseDn != null && baseDn.equals(tokensBaseDn); - } - -// protected Dictionary currentState() { -// Dictionary res = new Hashtable(); -// // res.put(NodeConstants.CN, NodeConstants.DEFAULT); -// for (LdapName name : businessRoles.keySet()) { -// AbstractUserDirectory userDirectory = businessRoles.get(name); -// String uri = UserAdminConf.propertiesAsUri(userDirectory.getProperties()).toString(); -// res.put(uri, ""); -// } -// return res; -// } - - public void destroy() { - for (LdapName name : businessRoles.keySet()) { - AbstractUserDirectory userDirectory = businessRoles.get(name); - destroy(userDirectory); - } - businessRoles.clear(); - businessRoles = null; - destroy(systemRoles); - systemRoles = null; - } - - private void destroy(AbstractUserDirectory userDirectory) { - preDestroy(userDirectory); - userDirectory.destroy(); - } - - protected void removeUserDirectory(LdapName baseDn) { - if (isSystemRolesBaseDn(baseDn)) - throw new UserDirectoryException("System roles cannot be removed "); - if (!businessRoles.containsKey(baseDn)) - throw new UserDirectoryException("No user directory registered for " + baseDn); - AbstractUserDirectory userDirectory = businessRoles.remove(baseDn); - destroy(userDirectory); - } - - /** - * Called before each user directory is destroyed, so that additional actions - * can be performed. - */ - protected void preDestroy(AbstractUserDirectory userDirectory) { - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AuthenticatingUser.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AuthenticatingUser.java deleted file mode 100644 index 01db8be98..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AuthenticatingUser.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.util.Dictionary; -import java.util.Hashtable; - -import javax.naming.ldap.LdapName; - -import org.osgi.service.useradmin.User; - -/** - * A special user type used during authentication in order to provide the - * credentials required for scoping the user admin. - */ -public class AuthenticatingUser implements User { - /** From com.sun.security.auth.module.*LoginModule */ - public final static String SHARED_STATE_NAME = "javax.security.auth.login.name"; - /** From com.sun.security.auth.module.*LoginModule */ - public final static String SHARED_STATE_PWD = "javax.security.auth.login.password"; - - private final String name; - private final Dictionary credentials; - - public AuthenticatingUser(LdapName name) { - if (name == null) - throw new NullPointerException("Provided name cannot be null."); - this.name = name.toString(); - this.credentials = new Hashtable<>(); - } - - public AuthenticatingUser(String name, Dictionary credentials) { - this.name = name; - this.credentials = credentials; - } - - public AuthenticatingUser(String name, char[] password) { - if (name == null) - throw new NullPointerException("Provided name cannot be null."); - this.name = name; - credentials = new Hashtable<>(); - credentials.put(SHARED_STATE_NAME, name); - byte[] pwd = DigestUtils.charsToBytes(password); - credentials.put(SHARED_STATE_PWD, pwd); - } - - @Override - public String getName() { - return name; - } - - @Override - public int getType() { - return User.USER; - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Override - public Dictionary getProperties() { - throw new UnsupportedOperationException(); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - @Override - public Dictionary getCredentials() { - return credentials; - } - - @Override - public boolean hasCredential(String key, Object value) { - throw new UnsupportedOperationException(); - } - - @Override - public int hashCode() { - return name.hashCode(); - } - - @Override - public String toString() { - return "Authenticating user " + name; - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DigestUtils.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DigestUtils.java deleted file mode 100644 index 511c2fede..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DigestUtils.java +++ /dev/null @@ -1,111 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.math.BigInteger; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.security.spec.KeySpec; -import java.util.Arrays; - -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; - -/** Utilities around digests, mostly those related to passwords. */ -class DigestUtils { - final static String PASSWORD_SCHEME_SHA = "SHA"; - final static String PASSWORD_SCHEME_PBKDF2_SHA256 = "PBKDF2_SHA256"; - - static byte[] sha1(byte[] bytes) { - try { - MessageDigest digest = MessageDigest.getInstance("SHA1"); - digest.update(bytes); - byte[] checksum = digest.digest(); - return checksum; - } catch (Exception e) { - throw new UserDirectoryException("Cannot SHA1 digest", e); - } - } - - static byte[] toPasswordScheme(String passwordScheme, char[] password, byte[] salt, Integer iterations, - Integer keyLength) { - try { - if (PASSWORD_SCHEME_SHA.equals(passwordScheme)) { - MessageDigest digest = MessageDigest.getInstance("SHA1"); - byte[] bytes = charsToBytes(password); - digest.update(bytes); - return digest.digest(); - } else if (PASSWORD_SCHEME_PBKDF2_SHA256.equals(passwordScheme)) { - KeySpec spec = new PBEKeySpec(password, salt, iterations, keyLength); - - SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); - final int ITERATION_LENGTH = 4; - byte[] key = f.generateSecret(spec).getEncoded(); - byte[] result = new byte[ITERATION_LENGTH + salt.length + key.length]; - byte iterationsArr[] = new BigInteger(iterations.toString()).toByteArray(); - if (iterationsArr.length < ITERATION_LENGTH) { - Arrays.fill(result, 0, ITERATION_LENGTH - iterationsArr.length, (byte) 0); - System.arraycopy(iterationsArr, 0, result, ITERATION_LENGTH - iterationsArr.length, - iterationsArr.length); - } else { - System.arraycopy(iterationsArr, 0, result, 0, ITERATION_LENGTH); - } - System.arraycopy(salt, 0, result, ITERATION_LENGTH, salt.length); - System.arraycopy(key, 0, result, ITERATION_LENGTH + salt.length, key.length); - return result; - } else { - throw new UnsupportedOperationException("Unkown password scheme " + passwordScheme); - } - } catch (Exception e) { - throw new UserDirectoryException("Cannot digest", e); - } - } - - static char[] bytesToChars(Object obj) { - if (obj instanceof char[]) - return (char[]) obj; - if (!(obj instanceof byte[])) - throw new IllegalArgumentException(obj.getClass() + " is not a byte array"); - ByteBuffer fromBuffer = ByteBuffer.wrap((byte[]) obj); - CharBuffer toBuffer = StandardCharsets.UTF_8.decode(fromBuffer); - char[] res = Arrays.copyOfRange(toBuffer.array(), toBuffer.position(), toBuffer.limit()); - // Arrays.fill(fromBuffer.array(), (byte) 0); // clear sensitive data - // Arrays.fill((byte[]) obj, (byte) 0); // clear sensitive data - // Arrays.fill(toBuffer.array(), '\u0000'); // clear sensitive data - return res; - } - - static byte[] charsToBytes(char[] chars) { - CharBuffer charBuffer = CharBuffer.wrap(chars); - ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer); - byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); - // Arrays.fill(charBuffer.array(), '\u0000'); // clear sensitive data - // Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data - return bytes; - } - - static String sha1str(String str) { - byte[] hash = sha1(str.getBytes(StandardCharsets.UTF_8)); - return encodeHexString(hash); - } - - final private static char[] hexArray = "0123456789abcdef".toCharArray(); - - /** - * From - * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to - * -a-hex-string-in-java - */ - public static String encodeHexString(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = hexArray[v >>> 4]; - hexChars[j * 2 + 1] = hexArray[v & 0x0F]; - } - return new String(hexChars); - } - - private DigestUtils() { - } -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DirectoryGroup.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DirectoryGroup.java deleted file mode 100644 index 7f8046313..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DirectoryGroup.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.util.List; - -import javax.naming.ldap.LdapName; - -import org.osgi.service.useradmin.Group; - -/** A group in a user directroy. */ -interface DirectoryGroup extends Group, DirectoryUser { - List getMemberNames(); -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DirectoryUser.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DirectoryUser.java deleted file mode 100644 index 146b80578..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DirectoryUser.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.argeo.osgi.useradmin; - -import javax.naming.directory.Attributes; -import javax.naming.ldap.LdapName; - -import org.osgi.service.useradmin.User; - -/** A user in a user directory. */ -interface DirectoryUser extends User { - LdapName getDn(); - - Attributes getAttributes(); - - void publishAttributes(Attributes modifiedAttributes); -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/IpaUtils.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/IpaUtils.java deleted file mode 100644 index d56c06ac0..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/IpaUtils.java +++ /dev/null @@ -1,137 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Dictionary; -import java.util.Hashtable; -import java.util.List; - -import javax.naming.InvalidNameException; -import javax.naming.NamingException; -import javax.naming.ldap.LdapName; - -import org.argeo.naming.DnsBrowser; -import org.argeo.naming.LdapAttrs; - -/** Free IPA specific conventions. */ -public class IpaUtils { - public final static String IPA_USER_BASE = "cn=users,cn=accounts"; - public final static String IPA_GROUP_BASE = "cn=groups,cn=accounts"; - public final static String IPA_SERVICE_BASE = "cn=services,cn=accounts"; - - private final static String KRB_PRINCIPAL_NAME = LdapAttrs.krbPrincipalName.name().toLowerCase(); - - public final static String IPA_USER_DIRECTORY_CONFIG = UserAdminConf.userBase + "=" + IPA_USER_BASE + "&" - + UserAdminConf.groupBase + "=" + IPA_GROUP_BASE + "&" + UserAdminConf.readOnly + "=true"; - - @Deprecated - static String domainToUserDirectoryConfigPath(String realm) { - return domainToBaseDn(realm) + "?" + IPA_USER_DIRECTORY_CONFIG + "&" + UserAdminConf.realm.name() + "=" + realm; - } - - public static void addIpaConfig(String realm, Dictionary properties) { - properties.put(UserAdminConf.baseDn.name(), domainToBaseDn(realm)); - properties.put(UserAdminConf.realm.name(), realm); - properties.put(UserAdminConf.userBase.name(), IPA_USER_BASE); - properties.put(UserAdminConf.groupBase.name(), IPA_GROUP_BASE); - properties.put(UserAdminConf.readOnly.name(), Boolean.TRUE.toString()); - } - - public static String domainToBaseDn(String domain) { - String[] dcs = domain.split("\\."); - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < dcs.length; i++) { - if (i != 0) - sb.append(','); - String dc = dcs[i]; - sb.append(LdapAttrs.dc.name()).append('=').append(dc.toLowerCase()); - } - return sb.toString(); - } - - public static LdapName kerberosToDn(String kerberosName) { - String[] kname = kerberosName.split("@"); - String username = kname[0]; - String baseDn = domainToBaseDn(kname[1]); - String dn; - if (!username.contains("/")) - dn = LdapAttrs.uid + "=" + username + "," + IPA_USER_BASE + "," + baseDn; - else - dn = KRB_PRINCIPAL_NAME + "=" + kerberosName + "," + IPA_SERVICE_BASE + "," + baseDn; - try { - return new LdapName(dn); - } catch (InvalidNameException e) { - throw new IllegalArgumentException("Badly formatted name for " + kerberosName + ": " + dn); - } - } - - private IpaUtils() { - - } - - public static String kerberosDomainFromDns() { - String kerberosDomain; - try (DnsBrowser dnsBrowser = new DnsBrowser()) { - InetAddress localhost = InetAddress.getLocalHost(); - String hostname = localhost.getHostName(); - String dnsZone = hostname.substring(hostname.indexOf('.') + 1); - kerberosDomain = dnsBrowser.getRecord("_kerberos." + dnsZone, "TXT"); - return kerberosDomain; - } catch (Exception e) { - throw new UserDirectoryException("Cannot determine Kerberos domain from DNS", e); - } - - } - - public static Dictionary convertIpaUri(URI uri) { - String path = uri.getPath(); - String kerberosRealm; - if (path == null || path.length() <= 1) { - kerberosRealm = kerberosDomainFromDns(); - } else { - kerberosRealm = path.substring(1); - } - - if (kerberosRealm == null) - throw new UserDirectoryException("No Kerberos domain available for " + uri); - // TODO intergrate CA certificate in truststore - // String schemeToUse = SCHEME_LDAPS; - String schemeToUse = UserAdminConf.SCHEME_LDAP; - List ldapHosts; - String ldapHostsStr = uri.getHost(); - if (ldapHostsStr == null || ldapHostsStr.trim().equals("")) { - try (DnsBrowser dnsBrowser = new DnsBrowser()) { - ldapHosts = dnsBrowser.getSrvRecordsAsHosts("_ldap._tcp." + kerberosRealm.toLowerCase(), - schemeToUse.equals(UserAdminConf.SCHEME_LDAP) ? true : false); - if (ldapHosts == null || ldapHosts.size() == 0) { - throw new UserDirectoryException("Cannot configure LDAP for IPA " + uri); - } else { - ldapHostsStr = ldapHosts.get(0); - } - } catch (NamingException | IOException e) { - throw new UserDirectoryException("cannot convert IPA uri " + uri, e); - } - } else { - ldapHosts = new ArrayList<>(); - ldapHosts.add(ldapHostsStr); - } - - StringBuilder uriStr = new StringBuilder(); - try { - for (String host : ldapHosts) { - URI convertedUri = new URI(schemeToUse + "://" + host + "/"); - uriStr.append(convertedUri).append(' '); - } - } catch (URISyntaxException e) { - throw new UserDirectoryException("cannot convert IPA uri " + uri, e); - } - - Hashtable res = new Hashtable<>(); - res.put(UserAdminConf.uri.name(), uriStr.toString()); - addIpaConfig(kerberosRealm, res); - return res; - } -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdapConnection.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdapConnection.java deleted file mode 100644 index 3e869f3b0..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdapConnection.java +++ /dev/null @@ -1,147 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.util.Dictionary; -import java.util.Hashtable; - -import javax.naming.CommunicationException; -import javax.naming.Context; -import javax.naming.NameNotFoundException; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attributes; -import javax.naming.directory.DirContext; -import javax.naming.directory.SearchControls; -import javax.naming.directory.SearchResult; -import javax.naming.ldap.InitialLdapContext; -import javax.naming.ldap.LdapName; - -import org.argeo.naming.LdapAttrs; - -/** A synchronized wrapper for a single {@link InitialLdapContext}. */ -// TODO implement multiple contexts and connection pooling. -class LdapConnection { - private InitialLdapContext initialLdapContext = null; - - LdapConnection(String url, Dictionary properties) { - try { - Hashtable connEnv = new Hashtable(); - connEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); - connEnv.put(Context.PROVIDER_URL, url); - connEnv.put("java.naming.ldap.attributes.binary", LdapAttrs.userPassword.name()); - // use pooling in order to avoid connection timeout -// connEnv.put("com.sun.jndi.ldap.connect.pool", "true"); -// connEnv.put("com.sun.jndi.ldap.connect.pool.timeout", 300000); - - initialLdapContext = new InitialLdapContext(connEnv, null); - // StartTlsResponse tls = (StartTlsResponse) ctx - // .extendedOperation(new StartTlsRequest()); - // tls.negotiate(); - Object securityAuthentication = properties.get(Context.SECURITY_AUTHENTICATION); - if (securityAuthentication != null) - initialLdapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, securityAuthentication); - else - initialLdapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple"); - Object principal = properties.get(Context.SECURITY_PRINCIPAL); - if (principal != null) { - initialLdapContext.addToEnvironment(Context.SECURITY_PRINCIPAL, principal.toString()); - Object creds = properties.get(Context.SECURITY_CREDENTIALS); - if (creds != null) { - initialLdapContext.addToEnvironment(Context.SECURITY_CREDENTIALS, creds.toString()); - } - } - } catch (Exception e) { - throw new UserDirectoryException("Cannot connect to LDAP", e); - } - - } - - public void init() { - - } - - public void destroy() { - try { - // tls.close(); - initialLdapContext.close(); - initialLdapContext = null; - } catch (NamingException e) { - e.printStackTrace(); - } - } - - protected InitialLdapContext getLdapContext() { - return initialLdapContext; - } - - protected void reconnect() throws NamingException { - initialLdapContext.reconnect(initialLdapContext.getConnectControls()); - } - - public synchronized NamingEnumeration search(LdapName searchBase, String searchFilter, - SearchControls searchControls) throws NamingException { - NamingEnumeration results; - try { - results = getLdapContext().search(searchBase, searchFilter, searchControls); - } catch (CommunicationException e) { - reconnect(); - results = getLdapContext().search(searchBase, searchFilter, searchControls); - } - return results; - } - - public synchronized Attributes getAttributes(LdapName name) throws NamingException { - try { - return getLdapContext().getAttributes(name); - } catch (CommunicationException e) { - reconnect(); - return getLdapContext().getAttributes(name); - } - } - - synchronized void prepareChanges(UserDirectoryWorkingCopy wc) throws NamingException { - // make sure connection will work - reconnect(); - - // delete - for (LdapName dn : wc.getDeletedUsers().keySet()) { - if (!entryExists(dn)) - throw new UserDirectoryException("User to delete no found " + dn); - } - // add - for (LdapName dn : wc.getNewUsers().keySet()) { - if (entryExists(dn)) - throw new UserDirectoryException("User to create found " + dn); - } - // modify - for (LdapName dn : wc.getModifiedUsers().keySet()) { - if (!wc.getNewUsers().containsKey(dn) && !entryExists(dn)) - throw new UserDirectoryException("User to modify not found " + dn); - } - - } - - protected boolean entryExists(LdapName dn) throws NamingException { - try { - return getAttributes(dn).size() != 0; - } catch (NameNotFoundException e) { - return false; - } - } - - synchronized void commitChanges(UserDirectoryWorkingCopy wc) throws NamingException { - // delete - for (LdapName dn : wc.getDeletedUsers().keySet()) { - getLdapContext().destroySubcontext(dn); - } - // add - for (LdapName dn : wc.getNewUsers().keySet()) { - DirectoryUser user = wc.getNewUsers().get(dn); - getLdapContext().createSubcontext(dn, user.getAttributes()); - } - // modify - for (LdapName dn : wc.getModifiedUsers().keySet()) { - Attributes modifiedAttrs = wc.getModifiedUsers().get(dn); - getLdapContext().modifyAttributes(dn, DirContext.REPLACE_ATTRIBUTE, modifiedAttrs); - } - } -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdapUserAdmin.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdapUserAdmin.java deleted file mode 100644 index cd28748f5..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdapUserAdmin.java +++ /dev/null @@ -1,189 +0,0 @@ -package org.argeo.osgi.useradmin; - -import static org.argeo.naming.LdapAttrs.objectClass; - -import java.util.ArrayList; -import java.util.Dictionary; -import java.util.List; - -import javax.naming.AuthenticationNotSupportedException; -import javax.naming.Binding; -import javax.naming.Context; -import javax.naming.InvalidNameException; -import javax.naming.NameNotFoundException; -import javax.naming.NamingEnumeration; -import javax.naming.NamingException; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.directory.SearchControls; -import javax.naming.directory.SearchResult; -import javax.naming.ldap.LdapName; - -import org.osgi.framework.Filter; -import org.osgi.service.useradmin.Role; -import org.osgi.service.useradmin.User; - -/** A user admin based on a LDAP server. */ -public class LdapUserAdmin extends AbstractUserDirectory { - private LdapConnection ldapConnection; - - public LdapUserAdmin(Dictionary properties) { - this(properties, false); - } - - public LdapUserAdmin(Dictionary properties, boolean scoped) { - super(null, properties, scoped); - ldapConnection = new LdapConnection(getUri().toString(), properties); - } - - public void destroy() { - ldapConnection.destroy(); - } - - @Override - protected AbstractUserDirectory scope(User user) { - Dictionary credentials = user.getCredentials(); - String username = (String) credentials.get(SHARED_STATE_USERNAME); - if (username == null) - username = user.getName(); - Dictionary properties = cloneProperties(); - properties.put(Context.SECURITY_PRINCIPAL, username.toString()); - Object pwdCred = credentials.get(SHARED_STATE_PASSWORD); - byte[] pwd = (byte[]) pwdCred; - if (pwd != null) { - char[] password = DigestUtils.bytesToChars(pwd); - properties.put(Context.SECURITY_CREDENTIALS, new String(password)); - } else { - properties.put(Context.SECURITY_AUTHENTICATION, "GSSAPI"); - } - return new LdapUserAdmin(properties, true); - } - -// protected InitialLdapContext getLdapContext() { -// return initialLdapContext; -// } - - @Override - protected Boolean daoHasRole(LdapName dn) { - try { - return daoGetRole(dn) != null; - } catch (NameNotFoundException e) { - return false; - } - } - - @Override - protected DirectoryUser daoGetRole(LdapName name) throws NameNotFoundException { - try { - Attributes attrs = ldapConnection.getAttributes(name); - if (attrs.size() == 0) - return null; - int roleType = roleType(name); - LdifUser res; - if (roleType == Role.GROUP) - res = new LdifGroup(this, name, attrs); - else if (roleType == Role.USER) - res = new LdifUser(this, name, attrs); - else - throw new UserDirectoryException("Unsupported LDAP type for " + name); - return res; - } catch (NameNotFoundException e) { - throw e; - } catch (NamingException e) { - return null; - } - } - - @Override - protected List doGetRoles(Filter f) { - ArrayList res = new ArrayList(); - try { - String searchFilter = f != null ? f.toString() - : "(|(" + objectClass + "=" + getUserObjectClass() + ")(" + objectClass + "=" - + getGroupObjectClass() + "))"; - SearchControls searchControls = new SearchControls(); - searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); - - LdapName searchBase = getBaseDn(); - NamingEnumeration results = ldapConnection.search(searchBase, searchFilter, searchControls); - - results: while (results.hasMoreElements()) { - SearchResult searchResult = results.next(); - Attributes attrs = searchResult.getAttributes(); - Attribute objectClassAttr = attrs.get(objectClass.name()); - LdapName dn = toDn(searchBase, searchResult); - LdifUser role; - if (objectClassAttr.contains(getGroupObjectClass()) - || objectClassAttr.contains(getGroupObjectClass().toLowerCase())) - role = new LdifGroup(this, dn, attrs); - else if (objectClassAttr.contains(getUserObjectClass()) - || objectClassAttr.contains(getUserObjectClass().toLowerCase())) - role = new LdifUser(this, dn, attrs); - else { -// log.warn("Unsupported LDAP type for " + searchResult.getName()); - continue results; - } - res.add(role); - } - return res; - } catch (AuthenticationNotSupportedException e) { - // ignore (typically an unsupported anonymous bind) - // TODO better logging - return res; - } catch (Exception e) { - e.printStackTrace(); - throw new UserDirectoryException("Cannot get roles for filter " + f, e); - } - } - - private LdapName toDn(LdapName baseDn, Binding binding) throws InvalidNameException { - return new LdapName(binding.isRelative() ? binding.getName() + "," + baseDn : binding.getName()); - } - - @Override - protected List getDirectGroups(LdapName dn) { - List directGroups = new ArrayList(); - try { - String searchFilter = "(&(" + objectClass + "=" + getGroupObjectClass() + ")(" + getMemberAttributeId() - + "=" + dn + "))"; - - SearchControls searchControls = new SearchControls(); - searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); - - LdapName searchBase = getBaseDn(); - NamingEnumeration results = ldapConnection.search(searchBase, searchFilter, searchControls); - - while (results.hasMoreElements()) { - SearchResult searchResult = (SearchResult) results.nextElement(); - directGroups.add(toDn(searchBase, searchResult)); - } - return directGroups; - } catch (Exception e) { - throw new UserDirectoryException("Cannot populate direct members of " + dn, e); - } - } - - @Override - protected void prepare(UserDirectoryWorkingCopy wc) { - try { - ldapConnection.prepareChanges(wc); - } catch (NamingException e) { - throw new UserDirectoryException("Cannot prepare LDAP", e); - } - } - - @Override - protected void commit(UserDirectoryWorkingCopy wc) { - try { - ldapConnection.commitChanges(wc); - } catch (NamingException e) { - throw new UserDirectoryException("Cannot commit LDAP", e); - } - } - - @Override - protected void rollback(UserDirectoryWorkingCopy wc) { - // prepare not impacting - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifAuthorization.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifAuthorization.java deleted file mode 100644 index 354f8c0e2..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifAuthorization.java +++ /dev/null @@ -1,85 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.util.Arrays; -import java.util.Collections; -import java.util.Dictionary; -import java.util.List; - -import org.argeo.naming.LdapAttrs; -import org.osgi.service.useradmin.Authorization; -import org.osgi.service.useradmin.Role; -import org.osgi.service.useradmin.User; - -/** Basic authorization. */ -class LdifAuthorization implements Authorization { - private final String name; - private final String displayName; - private final List allRoles; - - public LdifAuthorization(User user, List allRoles) { - if (user == null) { - this.name = null; - this.displayName = "anonymous"; - } else { - this.name = user.getName(); - this.displayName = extractDisplayName(user); - } - // roles - String[] roles = new String[allRoles.size()]; - for (int i = 0; i < allRoles.size(); i++) { - roles[i] = allRoles.get(i).getName(); - } - this.allRoles = Collections.unmodifiableList(Arrays.asList(roles)); - } - - @Override - public String getName() { - return name; - } - - @Override - public boolean hasRole(String name) { - return allRoles.contains(name); - } - - @Override - public String[] getRoles() { - return allRoles.toArray(new String[allRoles.size()]); - } - - @Override - public int hashCode() { - if (name == null) - return super.hashCode(); - return name.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (!(obj instanceof Authorization)) - return false; - Authorization that = (Authorization) obj; - if (name == null) - return that.getName() == null; - return name.equals(that.getName()); - } - - @Override - public String toString() { - return displayName; - } - - final static String extractDisplayName(User user) { - Dictionary props = user.getProperties(); - Object displayName = props.get(LdapAttrs.displayName); - if (displayName == null) - displayName = props.get(LdapAttrs.cn); - if (displayName == null) - displayName = props.get(LdapAttrs.uid); - if (displayName == null) - displayName = user.getName(); - if (displayName == null) - throw new UserDirectoryException("Cannot set display name for " + user); - return displayName.toString(); - } -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifGroup.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifGroup.java deleted file mode 100644 index f4e558348..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifGroup.java +++ /dev/null @@ -1,124 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.util.ArrayList; -import java.util.List; - -import javax.naming.InvalidNameException; -import javax.naming.NamingEnumeration; -import javax.naming.directory.Attribute; -import javax.naming.directory.Attributes; -import javax.naming.ldap.LdapName; - -import org.osgi.service.useradmin.Role; - -/** Directory group implementation */ -class LdifGroup extends LdifUser implements DirectoryGroup { - private final String memberAttributeId; - - LdifGroup(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes) { - super(userAdmin, dn, attributes); - memberAttributeId = userAdmin.getMemberAttributeId(); - } - - @Override - public boolean addMember(Role role) { - try { - Role foundRole = findRole(new LdapName(role.getName())); - if (foundRole == null) - throw new UnsupportedOperationException( - "Adding role " + role.getName() + " is unsupported within this context."); - } catch (InvalidNameException e) { - throw new IllegalArgumentException("Role name" + role.getName() + " is badly formatted"); - } - - getUserAdmin().checkEdit(); - if (!isEditing()) - startEditing(); - - Attribute member = getAttributes().get(memberAttributeId); - if (member != null) { - if (member.contains(role.getName())) - return false; - else - member.add(role.getName()); - } else - getAttributes().put(memberAttributeId, role.getName()); - return true; - } - - @Override - public boolean addRequiredMember(Role role) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean removeMember(Role role) { - getUserAdmin().checkEdit(); - if (!isEditing()) - startEditing(); - - Attribute member = getAttributes().get(memberAttributeId); - if (member != null) { - if (!member.contains(role.getName())) - return false; - member.remove(role.getName()); - return true; - } else - return false; - } - - @Override - public Role[] getMembers() { - List directMembers = new ArrayList(); - for (LdapName ldapName : getMemberNames()) { - Role role = findRole(ldapName); - if (role == null) { - throw new UserDirectoryException("Role " + ldapName + " cannot be added."); - } - directMembers.add(role); - } - return directMembers.toArray(new Role[directMembers.size()]); - } - - /** - * Whether a role with this name can be found from this context. - * - * @return The related {@link Role} or null. - */ - protected Role findRole(LdapName ldapName) { - Role role = getUserAdmin().getRole(ldapName.toString()); - if (role == null) { - if (getUserAdmin().getExternalRoles() != null) - role = getUserAdmin().getExternalRoles().getRole(ldapName.toString()); - } - return role; - } - - @Override - public List getMemberNames() { - Attribute memberAttribute = getAttributes().get(memberAttributeId); - 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 (Exception e) { - throw new UserDirectoryException("Cannot get members", e); - } - } - - @Override - public Role[] getRequiredMembers() { - throw new UnsupportedOperationException(); - } - - @Override - public int getType() { - return GROUP; - } -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java deleted file mode 100644 index b3e7f5955..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java +++ /dev/null @@ -1,409 +0,0 @@ -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.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Set; - -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.naming.AuthPassword; -import org.argeo.naming.LdapAttrs; -import org.argeo.naming.SharedSecret; - -/** Directory user implementation */ -class LdifUser implements DirectoryUser { - private final AbstractUserDirectory userAdmin; - - private final LdapName dn; - - private final boolean frozen; - private Attributes publishedAttributes; - - private final AttributeDictionary properties; - private final AttributeDictionary credentials; - - LdifUser(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes) { - this(userAdmin, dn, attributes, false); - } - - private LdifUser(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes, boolean frozen) { - this.userAdmin = userAdmin; - this.dn = dn; - this.publishedAttributes = attributes; - properties = new AttributeDictionary(false); - credentials = new AttributeDictionary(true); - this.frozen = frozen; - } - - @Override - public String getName() { - return dn.toString(); - } - - @Override - public int getType() { - 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 = DigestUtils.bytesToChars(value); - 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(), DigestUtils.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 = DigestUtils.bytesToChars((byte[]) value); - byte[] valueBytes; - if (DigestUtils.PASSWORD_SCHEME_SHA.equals(passwordScheme)) { - valueBytes = DigestUtils.toPasswordScheme(passwordScheme, passwordValue, null, null, null); - } else if (DigestUtils.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 = DigestUtils.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(DigestUtils.sha1(DigestUtils.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; -// } - - @Override - public LdapName getDn() { - return dn; - } - - @Override - public synchronized Attributes getAttributes() { - return isEditing() ? getModifiedAttributes() : publishedAttributes; - } - - /** Should only be called from working copy thread. */ - private synchronized Attributes getModifiedAttributes() { - assert getWc() != null; - return getWc().getAttributes(getDn()); - } - - protected synchronized boolean isEditing() { - return getWc() != null && getModifiedAttributes() != null; - } - - private synchronized UserDirectoryWorkingCopy getWc() { - return userAdmin.getWorkingCopy(); - } - - protected synchronized void startEditing() { - if (frozen) - throw new UserDirectoryException("Cannot edit frozen view"); - if (getUserAdmin().isReadOnly()) - throw new UserDirectoryException("User directory is read-only"); - assert getModifiedAttributes() == null; - getWc().startEditing(this); - // modifiedAttributes = (Attributes) publishedAttributes.clone(); - } - - public synchronized void publishAttributes(Attributes modifiedAttributes) { - publishedAttributes = modifiedAttributes; - } - - public DirectoryUser getPublished() { - return new LdifUser(userAdmin, dn, publishedAttributes, true); - } - - @Override - public int hashCode() { - return dn.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj instanceof LdifUser) { - LdifUser that = (LdifUser) obj; - return this.dn.equals(that.dn); - } - return false; - } - - @Override - public String toString() { - return dn.toString(); - } - - protected AbstractUserDirectory getUserAdmin() { - return userAdmin; - } - - private class AttributeDictionary extends Dictionary { - private final List effectiveKeys = new ArrayList(); - private final List attrFilter; - private final Boolean includeFilter; - - public AttributeDictionary(Boolean includeFilter) { - this.attrFilter = userAdmin.getCredentialAttributeIds(); - this.includeFilter = includeFilter; - try { - NamingEnumeration ids = getAttributes().getIDs(); - while (ids.hasMore()) { - String id = ids.next(); - if (includeFilter && attrFilter.contains(id)) - effectiveKeys.add(id); - else if (!includeFilter && !attrFilter.contains(id)) - effectiveKeys.add(id); - } - } catch (NamingException e) { - throw new UserDirectoryException("Cannot initialise attribute dictionary", e); - } - } - - @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 = getAttributes().get(key.toString()); - 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; - if (!attr.getID().equals(LdapAttrs.objectClass.name())) - return value; - // special case for object class - NamingEnumeration en = attr.getAll(); - Set objectClasses = new HashSet(); - while (en.hasMore()) { - String objectClass = en.next().toString(); - objectClasses.add(objectClass); - } - - if (objectClasses.contains(userAdmin.getUserObjectClass())) - return userAdmin.getUserObjectClass(); - else if (objectClasses.contains(userAdmin.getGroupObjectClass())) - return userAdmin.getGroupObjectClass(); - else - return value; - } catch (NamingException e) { - throw new UserDirectoryException("Cannot get value for attribute " + key, e); - } - } - - @Override - public Object put(String key, Object value) { - if (key == null) { - // TODO persist to other sources (like PKCS12) - char[] password = DigestUtils.bytesToChars(value); - byte[] hashedPassword = sha1hash(password); - return put(LdapAttrs.userPassword.name(), hashedPassword); - } - if (key.startsWith("X-")) { - return put(LdapAttrs.authPassword.name(), value); - } - - userAdmin.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 UserDirectoryException("Cannot get value for attribute " + key, e); - } - } - - @Override - public Object remove(Object key) { - userAdmin.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 UserDirectoryException("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; - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUserAdmin.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUserAdmin.java deleted file mode 100644 index c32bbc53f..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUserAdmin.java +++ /dev/null @@ -1,260 +0,0 @@ -package org.argeo.osgi.useradmin; - -import static org.argeo.naming.LdapAttrs.objectClass; -import static org.argeo.naming.LdapObjs.inetOrgPerson; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Dictionary; -import java.util.HashSet; -import java.util.Hashtable; -import java.util.List; -import java.util.Set; -import java.util.SortedMap; -import java.util.TreeMap; - -import javax.naming.NameNotFoundException; -import javax.naming.NamingEnumeration; -import javax.naming.directory.Attributes; -import javax.naming.ldap.LdapName; - -import org.argeo.naming.LdifParser; -import org.argeo.naming.LdifWriter; -import org.osgi.framework.Filter; -import org.osgi.service.useradmin.Role; -import org.osgi.service.useradmin.User; - -/** A user admin based on a LDIF files. */ -public class LdifUserAdmin extends AbstractUserDirectory { - private SortedMap users = new TreeMap(); - private SortedMap groups = new TreeMap(); - - public LdifUserAdmin(String uri, String baseDn) { - this(fromUri(uri, baseDn), false); - } - - public LdifUserAdmin(Dictionary properties) { - this(properties, false); - } - - protected LdifUserAdmin(Dictionary properties, boolean scoped) { - super(null, properties, scoped); - } - - public LdifUserAdmin(URI uri, Dictionary properties) { - super(uri, properties, false); - } - - @Override - protected AbstractUserDirectory scope(User user) { - Dictionary credentials = user.getCredentials(); - String username = (String) credentials.get(SHARED_STATE_USERNAME); - if (username == null) - username = user.getName(); - Object pwdCred = credentials.get(SHARED_STATE_PASSWORD); - byte[] pwd = (byte[]) pwdCred; - if (pwd != null) { - char[] password = DigestUtils.bytesToChars(pwd); - User directoryUser = (User) getRole(username); - if (!directoryUser.hasCredential(null, password)) - throw new UserDirectoryException("Invalid credentials"); - } else { - throw new UserDirectoryException("Password is required"); - } - Dictionary properties = cloneProperties(); - properties.put(UserAdminConf.readOnly.name(), "true"); - LdifUserAdmin scopedUserAdmin = new LdifUserAdmin(properties, true); - scopedUserAdmin.groups = Collections.unmodifiableSortedMap(groups); - scopedUserAdmin.users = Collections.unmodifiableSortedMap(users); - return scopedUserAdmin; - } - - private static Dictionary fromUri(String uri, String baseDn) { - Hashtable res = new Hashtable(); - res.put(UserAdminConf.uri.name(), uri); - res.put(UserAdminConf.baseDn.name(), baseDn); - return res; - } - - public void init() { - - try { - URI u = new URI(getUri()); - if (u.getScheme().equals("file")) { - File file = new File(u); - if (!file.exists()) - return; - } - load(u.toURL().openStream()); - } catch (Exception e) { - throw new UserDirectoryException("Cannot open URL " + getUri(), e); - } - } - - public void save() { - if (getUri() == null) - throw new UserDirectoryException("Cannot save LDIF user admin: no URI is set"); - if (isReadOnly()) - throw new UserDirectoryException("Cannot save LDIF user admin: " + getUri() + " is read-only"); - try (FileOutputStream out = new FileOutputStream(new File(new URI(getUri())))) { - save(out); - } catch (IOException | URISyntaxException e) { - throw new UserDirectoryException("Cannot save user admin to " + getUri(), e); - } - } - - public void save(OutputStream out) throws IOException { - try { - LdifWriter ldifWriter = new LdifWriter(out); - for (LdapName name : groups.keySet()) - ldifWriter.writeEntry(name, groups.get(name).getAttributes()); - for (LdapName name : users.keySet()) - ldifWriter.writeEntry(name, users.get(name).getAttributes()); - } finally { - out.close(); - } - } - - protected void load(InputStream in) { - try { - users.clear(); - groups.clear(); - - LdifParser ldifParser = new LdifParser(); - SortedMap allEntries = ldifParser.read(in); - for (LdapName key : allEntries.keySet()) { - Attributes attributes = allEntries.get(key); - // check for inconsistency - Set lowerCase = new HashSet(); - NamingEnumeration ids = attributes.getIDs(); - while (ids.hasMoreElements()) { - String id = ids.nextElement().toLowerCase(); - if (lowerCase.contains(id)) - throw new UserDirectoryException(key + " has duplicate id " + id); - lowerCase.add(id); - } - - // analyse object classes - NamingEnumeration objectClasses = attributes.get(objectClass.name()).getAll(); - // System.out.println(key); - objectClasses: while (objectClasses.hasMore()) { - String objectClass = objectClasses.next().toString(); - // System.out.println(" " + objectClass); - if (objectClass.toLowerCase().equals(inetOrgPerson.name().toLowerCase())) { - users.put(key, new LdifUser(this, key, attributes)); - break objectClasses; - } else if (objectClass.toLowerCase().equals(getGroupObjectClass().toLowerCase())) { - groups.put(key, new LdifGroup(this, key, attributes)); - break objectClasses; - } - } - } - } catch (Exception e) { - throw new UserDirectoryException("Cannot load user admin service from LDIF", e); - } - } - - public void destroy() { - if (users == null || groups == null) - throw new UserDirectoryException("User directory " + getBaseDn() + " is already destroyed"); - users = null; - groups = null; - } - - @Override - protected DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException { - if (groups.containsKey(key)) - return groups.get(key); - if (users.containsKey(key)) - return users.get(key); - throw new NameNotFoundException(key + " not persisted"); - } - - @Override - protected Boolean daoHasRole(LdapName dn) { - return users.containsKey(dn) || groups.containsKey(dn); - } - - protected List doGetRoles(Filter f) { - ArrayList res = new ArrayList(); - if (f == null) { - res.addAll(users.values()); - res.addAll(groups.values()); - } else { - for (DirectoryUser user : users.values()) { - if (f.match(user.getProperties())) - res.add(user); - } - for (DirectoryUser group : groups.values()) - if (f.match(group.getProperties())) - res.add(group); - } - return res; - } - - @Override - protected List getDirectGroups(LdapName dn) { - List directGroups = new ArrayList(); - for (LdapName name : groups.keySet()) { - DirectoryGroup group = groups.get(name); - if (group.getMemberNames().contains(dn)) - directGroups.add(group.getDn()); - } - return directGroups; - } - - @Override - protected void prepare(UserDirectoryWorkingCopy wc) { - // delete - for (LdapName dn : wc.getDeletedUsers().keySet()) { - if (users.containsKey(dn)) - users.remove(dn); - else if (groups.containsKey(dn)) - groups.remove(dn); - else - throw new UserDirectoryException("User to delete not found " + dn); - } - // add - for (LdapName dn : wc.getNewUsers().keySet()) { - DirectoryUser user = wc.getNewUsers().get(dn); - if (users.containsKey(dn) || groups.containsKey(dn)) - throw new UserDirectoryException("User to create found " + dn); - else if (Role.USER == user.getType()) - users.put(dn, user); - else if (Role.GROUP == user.getType()) - groups.put(dn, (DirectoryGroup) user); - else - throw new UserDirectoryException("Unsupported role type " + user.getType() + " for new user " + dn); - } - // modify - for (LdapName dn : wc.getModifiedUsers().keySet()) { - Attributes modifiedAttrs = wc.getModifiedUsers().get(dn); - DirectoryUser user; - if (users.containsKey(dn)) - user = users.get(dn); - else if (groups.containsKey(dn)) - user = groups.get(dn); - else - throw new UserDirectoryException("User to modify no found " + dn); - user.publishAttributes(modifiedAttrs); - } - } - - @Override - protected void commit(UserDirectoryWorkingCopy wc) { - save(); - } - - @Override - protected void rollback(UserDirectoryWorkingCopy wc) { - init(); - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/OsUserDirectory.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/OsUserDirectory.java deleted file mode 100644 index fe1ca7643..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/OsUserDirectory.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.net.URI; -import java.util.ArrayList; -import java.util.Dictionary; -import java.util.List; - -import javax.naming.NameNotFoundException; -import javax.naming.NamingException; -import javax.naming.directory.Attributes; -import javax.naming.directory.BasicAttributes; -import javax.naming.ldap.LdapName; - -import org.argeo.naming.LdapAttrs; -import org.osgi.framework.Filter; -import org.osgi.service.useradmin.User; - -public class OsUserDirectory extends AbstractUserDirectory { - private final String osUsername = System.getProperty("user.name"); - private final LdapName osUserDn; - private final LdifUser osUser; - - public OsUserDirectory(URI uriArg, Dictionary props) { - super(uriArg, props, false); - try { - osUserDn = new LdapName(LdapAttrs.uid.name() + "=" + osUsername + "," + getUserBase() + "," + getBaseDn()); - Attributes attributes = new BasicAttributes(); - attributes.put(LdapAttrs.uid.name(), osUsername); - osUser = new LdifUser(this, osUserDn, attributes); - } catch (NamingException e) { - throw new UserDirectoryException("Cannot create system user", e); - } - } - - @Override - protected List getDirectGroups(LdapName dn) { - return new ArrayList<>(); - } - - @Override - protected Boolean daoHasRole(LdapName dn) { - return osUserDn.equals(dn); - } - - @Override - protected DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException { - if (osUserDn.equals(key)) - return osUser; - else - throw new NameNotFoundException("Not an OS role"); - } - - @Override - protected List doGetRoles(Filter f) { - List res = new ArrayList<>(); - if (f == null || f.match(osUser.getProperties())) - res.add(osUser); - return res; - } - - @Override - protected AbstractUserDirectory scope(User user) { - throw new UnsupportedOperationException(); - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/OsUserUtils.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/OsUserUtils.java deleted file mode 100644 index ad6bf8816..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/OsUserUtils.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.net.URISyntaxException; -import java.net.URL; -import java.security.NoSuchAlgorithmException; -import java.security.URIParameter; - -import javax.security.auth.Subject; -import javax.security.auth.login.Configuration; -import javax.security.auth.login.LoginContext; -import javax.security.auth.login.LoginException; - -public class OsUserUtils { - private static String LOGIN_CONTEXT_USER_NIX = "USER_NIX"; - private static String LOGIN_CONTEXT_USER_NT = "USER_NT"; - - public static String getOsUsername() { - return System.getProperty("user.name"); - } - - public static LoginContext loginAsSystemUser(Subject subject) { - try { - URL jaasConfigurationUrl = OsUserUtils.class.getClassLoader() - .getResource("org/argeo/osgi/useradmin/jaas-os.cfg"); - URIParameter uriParameter = new URIParameter(jaasConfigurationUrl.toURI()); - Configuration jaasConfiguration = Configuration.getInstance("JavaLoginConfig", uriParameter); - LoginContext lc = new LoginContext(isWindows() ? LOGIN_CONTEXT_USER_NT : LOGIN_CONTEXT_USER_NIX, subject, - null, jaasConfiguration); - lc.login(); - return lc; - } catch (URISyntaxException | NoSuchAlgorithmException | LoginException e) { - throw new RuntimeException("Cannot login as system user", e); - } - } - - public static void main(String args[]) { - Subject subject = new Subject(); - LoginContext loginContext = loginAsSystemUser(subject); - System.out.println(subject); - try { - loginContext.logout(); - } catch (LoginException e) { - // silent - } - } - - private static boolean isWindows() { - return System.getProperty("os.name").startsWith("Windows"); - } - - private OsUserUtils() { - } -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/TokenUtils.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/TokenUtils.java deleted file mode 100644 index 83c1d76f6..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/TokenUtils.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.argeo.osgi.useradmin; - -import static org.argeo.naming.LdapAttrs.description; -import static org.argeo.naming.LdapAttrs.owner; - -import java.security.Principal; -import java.time.Instant; -import java.util.HashSet; -import java.util.Set; - -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; -import javax.security.auth.Subject; - -import org.argeo.naming.NamingUtils; -import org.osgi.service.useradmin.Group; - -/** - * Canonically implements the Argeo token conventions. - */ -public class TokenUtils { - public static Set tokensUsed(Subject subject, String tokensBaseDn) { - Set res = new HashSet<>(); - for (Principal principal : subject.getPrincipals()) { - String name = principal.getName(); - if (name.endsWith(tokensBaseDn)) { - try { - LdapName ldapName = new LdapName(name); - String token = ldapName.getRdn(ldapName.size()).getValue().toString(); - res.add(token); - } catch (InvalidNameException e) { - throw new UserDirectoryException("Invalid principal " + principal, e); - } - } - } - return res; - } - - /** The user related to this token group */ - public static String userDn(Group tokenGroup) { - return (String) tokenGroup.getProperties().get(owner.name()); - } - - public static boolean isExpired(Group tokenGroup) { - return isExpired(tokenGroup, Instant.now()); - - } - - public static boolean isExpired(Group tokenGroup, Instant instant) { - String expiryDateStr = (String) tokenGroup.getProperties().get(description.name()); - if (expiryDateStr != null) { - Instant expiryDate = NamingUtils.ldapDateToInstant(expiryDateStr); - if (expiryDate.isBefore(instant)) { - return true; - } - } - return false; - } - -// private final String token; -// -// public TokenUtils(String token) { -// this.token = token; -// } -// -// public String getToken() { -// return token; -// } -// -// @Override -// public int hashCode() { -// return token.hashCode(); -// } -// -// @Override -// public boolean equals(Object obj) { -// if ((obj instanceof TokenUtils) && ((TokenUtils) obj).token.equals(token)) -// return true; -// return false; -// } -// -// @Override -// public String toString() { -// return "Token #" + hashCode(); -// } - -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserAdminConf.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserAdminConf.java deleted file mode 100644 index ec41978dc..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserAdminConf.java +++ /dev/null @@ -1,239 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.net.InetAddress; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.UnknownHostException; -import java.util.Dictionary; -import java.util.Hashtable; -import java.util.List; -import java.util.Map; - -import javax.naming.Context; -import javax.naming.ldap.LdapName; - -import org.argeo.naming.NamingUtils; - -/** Properties used to configure user admins. */ -public enum UserAdminConf { - /** Base DN (cannot be configured externally) */ - baseDn("dc=example,dc=com"), - - /** URI of the underlying resource (cannot be configured externally) */ - uri("ldap://localhost:10389"), - - /** User objectClass */ - userObjectClass("inetOrgPerson"), - - /** Relative base DN for users */ - userBase("ou=People"), - - /** Groups objectClass */ - groupObjectClass("groupOfNames"), - - /** Relative base DN for users */ - groupBase("ou=Groups"), - - /** Read-only source */ - readOnly(null), - - /** Disabled source */ - disabled(null), - - /** Authentication realm */ - realm(null); - - public final static String FACTORY_PID = "org.argeo.osgi.useradmin.config"; - - public final static String SCHEME_LDAP = "ldap"; - public final static String SCHEME_LDAPS = "ldaps"; - public final static String SCHEME_FILE = "file"; - public final static String SCHEME_OS = "os"; - public final static String SCHEME_IPA = "ipa"; - - /** The default value. */ - private Object def; - - UserAdminConf(Object def) { - this.def = def; - } - - public Object getDefault() { - return def; - } - - /** - * For use as Java property. - * - * @deprecated use {@link #name()} instead - */ - @Deprecated - public String property() { - return name(); - } - - public String getValue(Dictionary properties) { - Object res = getRawValue(properties); - if (res == null) - return null; - return res.toString(); - } - - @SuppressWarnings("unchecked") - public T getRawValue(Dictionary properties) { - Object res = properties.get(name()); - if (res == null) - res = getDefault(); - return (T) res; - } - - /** @deprecated use {@link #valueOf(String)} instead */ - @Deprecated - public static UserAdminConf local(String property) { - return UserAdminConf.valueOf(property); - } - - /** Hides host and credentials. */ - public static URI propertiesAsUri(Dictionary properties) { - StringBuilder query = new StringBuilder(); - - boolean first = true; -// for (Enumeration keys = properties.keys(); keys.hasMoreElements();) { -// String key = keys.nextElement(); -// // TODO clarify which keys are relevant (list only the enum?) -// if (!key.equals("service.factoryPid") && !key.equals("cn") && !key.equals("dn") -// && !key.equals(Constants.SERVICE_PID) && !key.startsWith("java") && !key.equals(baseDn.name()) -// && !key.equals(uri.name()) && !key.equals(Constants.OBJECTCLASS) -// && !key.equals(Constants.SERVICE_ID) && !key.equals("bundle.id")) { -// if (first) -// first = false; -// else -// query.append('&'); -// query.append(valueOf(key).name()); -// query.append('=').append(properties.get(key).toString()); -// } -// } - - keys: for (UserAdminConf key : UserAdminConf.values()) { - if (key.equals(baseDn) || key.equals(uri)) - continue keys; - Object value = properties.get(key.name()); - if (value == null) - continue keys; - if (first) - first = false; - else - query.append('&'); - query.append(key.name()); - query.append('=').append(value.toString()); - - } - - Object bDnObj = properties.get(baseDn.name()); - String bDn = bDnObj != null ? bDnObj.toString() : null; - try { - return new URI(null, null, bDn != null ? '/' + bDn : null, query.length() != 0 ? query.toString() : null, - null); - } catch (URISyntaxException e) { - throw new UserDirectoryException("Cannot create URI from properties", e); - } - } - - public static Dictionary uriAsProperties(String uriStr) { - try { - Hashtable res = new Hashtable(); - URI u = new URI(uriStr); - String scheme = u.getScheme(); - if (scheme != null && scheme.equals(SCHEME_IPA)) { - return IpaUtils.convertIpaUri(u); -// scheme = u.getScheme(); - } - String path = u.getPath(); - // base DN - String bDn = path.substring(path.lastIndexOf('/') + 1, path.length()); - if (bDn.equals("") && SCHEME_OS.equals(scheme)) { - bDn = getBaseDnFromHostname(); - } - - if (bDn.endsWith(".ldif")) - bDn = bDn.substring(0, bDn.length() - ".ldif".length()); - - // Normalize base DN as LDAP name - bDn = new LdapName(bDn).toString(); - - String principal = null; - String credentials = null; - if (scheme != null) - if (scheme.equals(SCHEME_LDAP) || scheme.equals(SCHEME_LDAPS)) { - // TODO additional checks - if (u.getUserInfo() != null) { - String[] userInfo = u.getUserInfo().split(":"); - principal = userInfo.length > 0 ? userInfo[0] : null; - credentials = userInfo.length > 1 ? userInfo[1] : null; - } - } else if (scheme.equals(SCHEME_FILE)) { - } else if (scheme.equals(SCHEME_IPA)) { - } else if (scheme.equals(SCHEME_OS)) { - } else - throw new UserDirectoryException("Unsupported scheme " + scheme); - Map> query = NamingUtils.queryToMap(u); - for (String key : query.keySet()) { - UserAdminConf ldapProp = UserAdminConf.valueOf(key); - List values = query.get(key); - if (values.size() == 1) { - res.put(ldapProp.name(), values.get(0)); - } else { - throw new UserDirectoryException("Only single values are supported"); - } - } - res.put(baseDn.name(), bDn); - if (SCHEME_OS.equals(scheme)) - res.put(readOnly.name(), "true"); - if (principal != null) - res.put(Context.SECURITY_PRINCIPAL, principal); - if (credentials != null) - res.put(Context.SECURITY_CREDENTIALS, credentials); - if (scheme != null) {// relative URIs are dealt with externally - if (SCHEME_OS.equals(scheme)) { - res.put(uri.name(), SCHEME_OS + ":///"); - } else { - URI bareUri = new URI(scheme, null, u.getHost(), u.getPort(), - scheme.equals(SCHEME_FILE) ? u.getPath() : null, null, null); - res.put(uri.name(), bareUri.toString()); - } - } - return res; - } catch (Exception e) { - throw new UserDirectoryException("Cannot convert " + uri + " to properties", e); - } - } - - private static String getBaseDnFromHostname() { - String hostname; - try { - hostname = InetAddress.getLocalHost().getHostName(); - } catch (UnknownHostException e) { - hostname = "localhost.localdomain"; - } - int dotIdx = hostname.indexOf('.'); - if (dotIdx >= 0) { - String domain = hostname.substring(dotIdx + 1, hostname.length()); - String bDn = ("." + domain).replaceAll("\\.", ",dc="); - bDn = bDn.substring(1, bDn.length()); - return bDn; - } else { - return "dc=" + hostname; - } - } - - /** - * Hash the base DN in order to have a deterministic string to be used as a cn - * for the underlying user directory. - */ - public static String baseDnHash(Dictionary properties) { - String bDn = (String) properties.get(baseDn.name()); - if (bDn == null) - throw new UserDirectoryException("No baseDn in " + properties); - return DigestUtils.sha1str(bDn); - } -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserDirectory.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserDirectory.java deleted file mode 100644 index ff80c5ac8..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserDirectory.java +++ /dev/null @@ -1,25 +0,0 @@ -package org.argeo.osgi.useradmin; - -import javax.naming.ldap.LdapName; -import javax.transaction.xa.XAResource; - -/** Information about a user directory. */ -public interface UserDirectory { - /** The base DN of all entries in this user directory */ - LdapName getBaseDn(); - - /** The related {@link XAResource} */ - XAResource getXaResource(); - - boolean isReadOnly(); - - boolean isDisabled(); - - String getUserObjectClass(); - - String getUserBase(); - - String getGroupObjectClass(); - - String getGroupBase(); -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserDirectoryException.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserDirectoryException.java deleted file mode 100644 index 613d0fdf0..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserDirectoryException.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.argeo.osgi.useradmin; - -import org.osgi.service.useradmin.UserAdmin; - -/** - * Exceptions related to Argeo's implementation of OSGi {@link UserAdmin} - * service. - */ -public class UserDirectoryException extends RuntimeException { - private static final long serialVersionUID = 1419352360062048603L; - - public UserDirectoryException(String message) { - super(message); - } - - public UserDirectoryException(String message, Throwable e) { - super(message, e); - } -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserDirectoryWorkingCopy.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserDirectoryWorkingCopy.java deleted file mode 100644 index 0e25bdfa1..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/UserDirectoryWorkingCopy.java +++ /dev/null @@ -1,58 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.util.HashMap; -import java.util.Map; - -import javax.naming.directory.Attributes; -import javax.naming.ldap.LdapName; -import javax.transaction.xa.XAResource; - -/** {@link XAResource} for a user directory being edited. */ -class UserDirectoryWorkingCopy { - // private final static Log log = LogFactory - // .getLog(UserDirectoryWorkingCopy.class); - - private Map newUsers = new HashMap(); - private Map modifiedUsers = new HashMap(); - private Map deletedUsers = new HashMap(); - - void cleanUp() { - // clean collections - newUsers.clear(); - newUsers = null; - modifiedUsers.clear(); - modifiedUsers = null; - deletedUsers.clear(); - deletedUsers = null; - } - - public boolean noModifications() { - return newUsers.size() == 0 && modifiedUsers.size() == 0 - && deletedUsers.size() == 0; - } - - public Attributes getAttributes(LdapName dn) { - if (modifiedUsers.containsKey(dn)) - return modifiedUsers.get(dn); - return null; - } - - public void startEditing(DirectoryUser user) { - LdapName dn = user.getDn(); - if (modifiedUsers.containsKey(dn)) - throw new UserDirectoryException("Already editing " + dn); - modifiedUsers.put(dn, (Attributes) user.getAttributes().clone()); - } - - public Map getNewUsers() { - return newUsers; - } - - public Map getDeletedUsers() { - return deletedUsers; - } - - public Map getModifiedUsers() { - return modifiedUsers; - } -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/WcXaResource.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/WcXaResource.java deleted file mode 100644 index 1630b6bd3..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/WcXaResource.java +++ /dev/null @@ -1,135 +0,0 @@ -package org.argeo.osgi.useradmin; - -import java.util.HashMap; -import java.util.Map; - -import javax.transaction.xa.XAException; -import javax.transaction.xa.XAResource; -import javax.transaction.xa.Xid; - -/** {@link XAResource} for a user directory being edited. */ -class WcXaResource implements XAResource { - private final AbstractUserDirectory userDirectory; - - private Map workingCopies = new HashMap(); - private Xid editingXid = null; - private int transactionTimeout = 0; - - public WcXaResource(AbstractUserDirectory userDirectory) { - this.userDirectory = userDirectory; - } - - @Override - public synchronized void start(Xid xid, int flags) throws XAException { - if (editingXid != null) - throw new UserDirectoryException("Already editing " + editingXid); - UserDirectoryWorkingCopy wc = workingCopies.put(xid, new UserDirectoryWorkingCopy()); - if (wc != null) - throw new UserDirectoryException("There is already a working copy for " + xid); - this.editingXid = xid; - } - - @Override - public void end(Xid xid, int flags) throws XAException { - checkXid(xid); - } - - private UserDirectoryWorkingCopy wc(Xid xid) { - return workingCopies.get(xid); - } - - synchronized UserDirectoryWorkingCopy wc() { - if (editingXid == null) - return null; - UserDirectoryWorkingCopy wc = workingCopies.get(editingXid); - if (wc == null) - throw new UserDirectoryException("No working copy found for " + editingXid); - return wc; - } - - private synchronized void cleanUp(Xid xid) { - wc(xid).cleanUp(); - workingCopies.remove(xid); - editingXid = null; - } - - @Override - public int prepare(Xid xid) throws XAException { - checkXid(xid); - UserDirectoryWorkingCopy wc = wc(xid); - if (wc.noModifications()) - return XA_RDONLY; - try { - userDirectory.prepare(wc); - } catch (Exception e) { - e.printStackTrace(); - throw new XAException(XAException.XAER_RMERR); - } - return XA_OK; - } - - @Override - public void commit(Xid xid, boolean onePhase) throws XAException { - try { - checkXid(xid); - UserDirectoryWorkingCopy wc = wc(xid); - if (wc.noModifications()) - return; - if (onePhase) - userDirectory.prepare(wc); - userDirectory.commit(wc); - } catch (Exception e) { - e.printStackTrace(); - throw new XAException(XAException.XAER_RMERR); - } finally { - cleanUp(xid); - } - } - - @Override - public void rollback(Xid xid) throws XAException { - try { - checkXid(xid); - userDirectory.rollback(wc(xid)); - } catch (Exception e) { - e.printStackTrace(); - throw new XAException(XAException.XAER_RMERR); - } finally { - cleanUp(xid); - } - } - - @Override - public void forget(Xid xid) throws XAException { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isSameRM(XAResource xares) throws XAException { - return xares == this; - } - - @Override - public Xid[] recover(int flag) throws XAException { - return new Xid[0]; - } - - @Override - public int getTransactionTimeout() throws XAException { - return transactionTimeout; - } - - @Override - public boolean setTransactionTimeout(int seconds) throws XAException { - transactionTimeout = seconds; - return true; - } - - private void checkXid(Xid xid) throws XAException { - if (xid == null) - throw new XAException(XAException.XAER_OUTSIDE); - if (!xid.equals(xid)) - throw new XAException(XAException.XAER_NOTA); - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/jaas-os.cfg b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/jaas-os.cfg deleted file mode 100644 index da04505a7..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/jaas-os.cfg +++ /dev/null @@ -1,8 +0,0 @@ -USER_NIX { - com.sun.security.auth.module.UnixLoginModule requisite; -}; - -USER_NT { - com.sun.security.auth.module.NTLoginModule requisite; -}; - diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/package-info.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/package-info.java deleted file mode 100644 index c108d2c55..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** LDAP and LDIF based OSGi useradmin implementation. */ -package org.argeo.osgi.useradmin; \ No newline at end of file diff --git a/org.argeo.enterprise/src/org/argeo/osgi/util/FilterRequirement.java b/org.argeo.enterprise/src/org/argeo/osgi/util/FilterRequirement.java deleted file mode 100644 index 31f1d4de6..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/util/FilterRequirement.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.argeo.osgi.util; - -import java.util.HashMap; -import java.util.Map; - -import org.osgi.resource.Namespace; -import org.osgi.resource.Requirement; -import org.osgi.resource.Resource; - -/** Simplify filtering resources. */ -public class FilterRequirement implements Requirement { - private String namespace; - private String filter; - - public FilterRequirement(String namespace, String filter) { - this.namespace = namespace; - this.filter = filter; - } - - @Override - public Resource getResource() { - return null; - } - - @Override - public String getNamespace() { - return namespace; - } - - @Override - public Map getDirectives() { - Map directives = new HashMap<>(); - directives.put(Namespace.REQUIREMENT_FILTER_DIRECTIVE, filter); - return directives; - } - - @Override - public Map getAttributes() { - return new HashMap<>(); - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/util/OnServiceRegistration.java b/org.argeo.enterprise/src/org/argeo/osgi/util/OnServiceRegistration.java deleted file mode 100644 index 5a6760e0f..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/util/OnServiceRegistration.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.argeo.osgi.util; - -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.function.Function; - -import org.osgi.framework.BundleContext; -import org.osgi.framework.FrameworkUtil; -import org.osgi.framework.ServiceReference; -import org.osgi.util.tracker.ServiceTracker; - -public class OnServiceRegistration implements Future { - private BundleContext ownBundleContext = FrameworkUtil.getBundle(OnServiceRegistration.class).getBundleContext(); - - private ServiceTracker st; - - private R result; - private boolean cancelled = false; - private Throwable exception; - - public OnServiceRegistration(Class clss, Function function) { - this(null, clss, function); - } - - public OnServiceRegistration(BundleContext bundleContext, Class clss, Function function) { - st = new ServiceTracker(bundleContext != null ? bundleContext : ownBundleContext, clss, null) { - - @Override - public T addingService(ServiceReference reference) { - T service = super.addingService(reference); - try { - if (result != null)// we only want the first one - return service; - result = function.apply(service); - return service; - } catch (Exception e) { - exception = e; - return service; - } finally { - close(); - } - } - }; - st.open(bundleContext == null); - } - - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - if (result != null || exception != null || cancelled) - return false; - st.close(); - cancelled = true; - return true; - } - - @Override - public boolean isCancelled() { - return cancelled; - } - - @Override - public boolean isDone() { - return result != null || cancelled; - } - - @Override - public R get() throws InterruptedException, ExecutionException { - st.waitForService(0); - return tryGetResult(); - } - - @Override - public R get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { - st.waitForService(TimeUnit.MILLISECONDS.convert(timeout, unit)); - if (result == null) - throw new TimeoutException("No result after " + timeout + " " + unit); - return tryGetResult(); - } - - protected R tryGetResult() throws ExecutionException, CancellationException { - if (cancelled) - throw new CancellationException(); - if (exception != null) - throw new ExecutionException(exception); - if (result == null)// this should not happen - try { - throw new IllegalStateException("No result available"); - } catch (Exception e) { - exception = e; - throw new ExecutionException(e); - } - return result; - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/osgi/util/OsgiRegister.java b/org.argeo.enterprise/src/org/argeo/osgi/util/OsgiRegister.java deleted file mode 100644 index 7132b7c3f..000000000 --- a/org.argeo.enterprise/src/org/argeo/osgi/util/OsgiRegister.java +++ /dev/null @@ -1,60 +0,0 @@ -package org.argeo.osgi.util; - -import java.util.ArrayList; -import java.util.Hashtable; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executor; -import java.util.concurrent.ForkJoinPool; - -import org.argeo.util.register.Register; -import org.argeo.util.register.Singleton; -import org.osgi.framework.BundleContext; -import org.osgi.framework.ServiceRegistration; - -public class OsgiRegister implements Register { - private final BundleContext bundleContext; - private Executor executor; - - private CompletableFuture shutdownStarting = new CompletableFuture(); - - public OsgiRegister(BundleContext bundleContext) { - this.bundleContext = bundleContext; - // TODO experiment with dedicated executors - this.executor = ForkJoinPool.commonPool(); - } - - @Override - public Singleton set(T obj, Class clss, Map attributes, Class... classes) { - CompletableFuture> srf = new CompletableFuture>(); - CompletableFuture postRegistration = CompletableFuture.supplyAsync(() -> { - List lst = new ArrayList<>(); - lst.add(clss.getName()); - for (Class c : classes) { - lst.add(c.getName()); - } - ServiceRegistration sr = bundleContext.registerService(lst.toArray(new String[lst.size()]), obj, - new Hashtable(attributes)); - srf.complete(sr); - return obj; - }, executor); - Singleton singleton = new Singleton(clss, postRegistration); - - shutdownStarting. // - thenCompose(singleton::prepareUnregistration). // - thenRunAsync(() -> { - try { - srf.get().unregister(); - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - }, executor); - return singleton; - } - - public void shutdown() { - shutdownStarting.complete(null); - } -} diff --git a/org.argeo.enterprise/src/org/argeo/util/CsvParser.java b/org.argeo.enterprise/src/org/argeo/util/CsvParser.java deleted file mode 100644 index b903f7722..000000000 --- a/org.argeo.enterprise/src/org/argeo/util/CsvParser.java +++ /dev/null @@ -1,242 +0,0 @@ -package org.argeo.util; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.io.UnsupportedEncodingException; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * Parses a CSV file interpreting the first line as a header. The - * {@link #parse(InputStream)} method and the setters are synchronized so that - * the object cannot be modified when parsing. - */ -public abstract class CsvParser { - private char separator = ','; - private char quote = '\"'; - - private Boolean noHeader = false; - private Boolean strictLineAsLongAsHeader = true; - - /** - * Actually process a parsed line. If - * {@link #setStrictLineAsLongAsHeader(Boolean)} is true (default) the header - * and the tokens are guaranteed to have the same size. - * - * @param lineNumber the current line number, starts at 1 (the header, if header - * processing is enabled, the first line otherwise) - * @param header the read-only header or null if - * {@link #setNoHeader(Boolean)} is true (default is false) - * @param tokens the parsed tokens - */ - protected abstract void processLine(Integer lineNumber, List header, List tokens); - - /** - * Parses the CSV file (stream is closed at the end) - * - * @param in the stream to parse - * - * @deprecated Use {@link #parse(InputStream, Charset)} instead. - */ - @Deprecated - public synchronized void parse(InputStream in) { - parse(in, (Charset) null); - } - - /** - * Parses the CSV file (stream is closed at the end) - * - * @param in the stream to parse - * @param encoding the encoding to use. - * - * @deprecated Use {@link #parse(InputStream, Charset)} instead. - */ - @Deprecated - public synchronized void parse(InputStream in, String encoding) { - Reader reader; - if (encoding == null) - reader = new InputStreamReader(in); - else - try { - reader = new InputStreamReader(in, encoding); - } catch (UnsupportedEncodingException e) { - throw new IllegalArgumentException(e); - } - parse(reader); - } - - /** - * Parses the CSV file (stream is closed at the end) - * - * @param in the stream to parse - * @param charset the charset to use - */ - public synchronized void parse(InputStream in, Charset charset) { - Reader reader; - if (charset == null) - reader = new InputStreamReader(in); - else - reader = new InputStreamReader(in, charset); - parse(reader); - } - - /** - * Parses the CSV file (stream is closed at the end) - * - * @param reader the reader to use (it will be buffered) - */ - public synchronized void parse(Reader reader) { - Integer lineCount = 0; - try (BufferedReader bufferedReader = new BufferedReader(reader)) { - List header = null; - if (!noHeader) { - String headerStr = bufferedReader.readLine(); - if (headerStr == null)// empty file - return; - lineCount++; - header = new ArrayList(); - StringBuffer currStr = new StringBuffer(""); - Boolean wasInquote = false; - while (parseLine(headerStr, header, currStr, wasInquote)) { - headerStr = bufferedReader.readLine(); - if (headerStr == null) - break; - wasInquote = true; - } - header = Collections.unmodifiableList(header); - } - - String line = null; - lines: while ((line = bufferedReader.readLine()) != null) { - line = preProcessLine(line); - if (line == null) { - // skip line - continue lines; - } - lineCount++; - List tokens = new ArrayList(); - StringBuffer currStr = new StringBuffer(""); - Boolean wasInquote = false; - sublines: while (parseLine(line, tokens, currStr, wasInquote)) { - line = bufferedReader.readLine(); - if (line == null) - break sublines; - wasInquote = true; - } - if (!noHeader && strictLineAsLongAsHeader) { - int headerSize = header.size(); - int tokenSize = tokens.size(); - if (tokenSize == 1 && line.trim().equals("")) - continue lines;// empty line - if (headerSize != tokenSize) { - throw new IllegalStateException("Token size " + tokenSize + " is different from header size " - + headerSize + " at line " + lineCount + ", line: " + line + ", header: " + header - + ", tokens: " + tokens); - } - } - processLine(lineCount, header, tokens); - } - } catch (IOException e) { - throw new RuntimeException("Cannot parse CSV file (line: " + lineCount + ")", e); - } - } - - /** - * Called before each (logical) line is processed, giving a change to modify it - * (typically for cleaning dirty files). To be overridden, return the line - * unchanged by default. Skip the line if 'null' is returned. - */ - protected String preProcessLine(String line) { - return line; - } - - /** - * Parses a line character by character for performance purpose - * - * @return whether to continue parsing this line - */ - protected Boolean parseLine(String str, List tokens, StringBuffer currStr, Boolean wasInquote) { - if (wasInquote) - currStr.append('\n'); - - char[] arr = str.toCharArray(); - boolean inQuote = wasInquote; - for (int i = 0; i < arr.length; i++) { - char c = arr[i]; - if (c == separator) { - if (!inQuote) { - tokens.add(currStr.toString()); -// currStr.delete(0, currStr.length()); - currStr.setLength(0); - currStr.trimToSize(); - } else { - // we don't remove separator that are in a quoted substring - // System.out - // .println("IN QUOTE, got a separator: [" + c + "]"); - currStr.append(c); - } - } else if (c == quote) { - if (inQuote && (i + 1) < arr.length && arr[i + 1] == quote) { - // case of double quote - currStr.append(quote); - i++; - } else {// standard - inQuote = inQuote ? false : true; - } - } else { - currStr.append(c); - } - } - - if (!inQuote) { - tokens.add(currStr.toString()); - // System.out.println("# TOKEN: " + currStr); - } - // if (inQuote) - // throw new ArgeoException("Missing quote at the end of the line " - // + str + " (parsed: " + tokens + ")"); - if (inQuote) - return true; - else - return false; - // return tokens; - } - - public char getSeparator() { - return separator; - } - - public synchronized void setSeparator(char separator) { - this.separator = separator; - } - - public char getQuote() { - return quote; - } - - public synchronized void setQuote(char quote) { - this.quote = quote; - } - - public Boolean getNoHeader() { - return noHeader; - } - - public synchronized void setNoHeader(Boolean noHeader) { - this.noHeader = noHeader; - } - - public Boolean getStrictLineAsLongAsHeader() { - return strictLineAsLongAsHeader; - } - - public synchronized void setStrictLineAsLongAsHeader(Boolean strictLineAsLongAsHeader) { - this.strictLineAsLongAsHeader = strictLineAsLongAsHeader; - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/util/CsvParserWithLinesAsMap.java b/org.argeo.enterprise/src/org/argeo/util/CsvParserWithLinesAsMap.java deleted file mode 100644 index 8eb6e9463..000000000 --- a/org.argeo.enterprise/src/org/argeo/util/CsvParserWithLinesAsMap.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.argeo.util; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * CSV parser allowing to process lines as maps whose keys are the header - * fields. - */ -public abstract class CsvParserWithLinesAsMap extends CsvParser { - - /** - * Actually processes a line. - * - * @param lineNumber the current line number, starts at 1 (the header, if header - * processing is enabled, the first lien otherwise) - * @param line the parsed tokens as a map whose keys are the header fields - */ - protected abstract void processLine(Integer lineNumber, Map line); - - protected final void processLine(Integer lineNumber, List header, List tokens) { - if (header == null) - throw new IllegalArgumentException("Only CSV with header is supported"); - Map line = new HashMap(); - for (int i = 0; i < header.size(); i++) { - String key = header.get(i); - String value = null; - if (i < tokens.size()) - value = tokens.get(i); - line.put(key, value); - } - processLine(lineNumber, line); - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/util/CsvWriter.java b/org.argeo.enterprise/src/org/argeo/util/CsvWriter.java deleted file mode 100644 index c3b3a3ad7..000000000 --- a/org.argeo.enterprise/src/org/argeo/util/CsvWriter.java +++ /dev/null @@ -1,156 +0,0 @@ -package org.argeo.util; - -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.UnsupportedEncodingException; -import java.io.Writer; -import java.nio.charset.Charset; -import java.util.Iterator; -import java.util.List; - -/** Write in CSV format. */ -public class CsvWriter { - private final Writer out; - - private char separator = ','; - private char quote = '\"'; - - /** - * Creates a CSV writer. - * - * @param out the stream to write to. Caller is responsible for closing it. - * - * @deprecated Use {@link #CsvWriter(OutputStream, Charset)} instead. - * - */ - @Deprecated - public CsvWriter(OutputStream out) { - this.out = new OutputStreamWriter(out); - } - - /** - * Creates a CSV writer. - * - * @param out the stream to write to. Caller is responsible for closing it. - * @param encoding the encoding to use. - * - * @deprecated Use {@link #CsvWriter(OutputStream, Charset)} instead. - */ - @Deprecated - public CsvWriter(OutputStream out, String encoding) { - try { - this.out = new OutputStreamWriter(out, encoding); - } catch (UnsupportedEncodingException e) { - throw new IllegalArgumentException(e); - } - } - - /** - * Creates a CSV writer. - * - * @param out the stream to write to. Caller is responsible for closing it. - * @param charset the charset to use - */ - public CsvWriter(OutputStream out, Charset charset) { - this.out = new OutputStreamWriter(out, charset); - } - - /** - * Creates a CSV writer. - * - * @param out the stream to write to. Caller is responsible for closing it. - */ - public CsvWriter(Writer writer) { - this.out = writer; - } - - /** - * Write a CSV line. Also used to write a header if needed (this is transparent - * for the CSV writer): simply call it first, before writing the lines. - */ - public void writeLine(List tokens) { - try { - Iterator it = tokens.iterator(); - while (it.hasNext()) { - Object obj = it.next(); - writeToken(obj != null ? obj.toString() : null); - if (it.hasNext()) - out.write(separator); - } - out.write('\n'); - out.flush(); - } catch (IOException e) { - throw new RuntimeException("Could not write " + tokens, e); - } - } - - /** - * Write a CSV line. Also used to write a header if needed (this is transparent - * for the CSV writer): simply call it first, before writing the lines. - */ - public void writeLine(Object[] tokens) { - try { - for (int i = 0; i < tokens.length; i++) { - if (tokens[i] == null) { - writeToken(null); - } else { - writeToken(tokens[i].toString()); - } - if (i != (tokens.length - 1)) - out.write(separator); - } - out.write('\n'); - out.flush(); - } catch (IOException e) { - throw new RuntimeException("Could not write " + tokens, e); - } - } - - protected void writeToken(String token) throws IOException { - if (token == null) { - // TODO configure how to deal with null - out.write(""); - return; - } - // +2 for possible quotes, another +2 assuming there would be an already - // quoted string where quotes needs to be duplicated - // another +2 for safety - // we don't want to increase buffer size while writing - StringBuffer buf = new StringBuffer(token.length() + 6); - char[] arr = token.toCharArray(); - boolean shouldQuote = false; - for (char c : arr) { - if (!shouldQuote) { - if (c == separator) - shouldQuote = true; - if (c == '\n') - shouldQuote = true; - } - - if (c == quote) { - shouldQuote = true; - // duplicate quote - buf.append(quote); - } - - // generic case - buf.append(c); - } - - if (shouldQuote == true) - out.write(quote); - out.write(buf.toString()); - if (shouldQuote == true) - out.write(quote); - } - - public void setSeparator(char separator) { - this.separator = separator; - } - - public void setQuote(char quote) { - this.quote = quote; - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/util/DictionaryKeys.java b/org.argeo.enterprise/src/org/argeo/util/DictionaryKeys.java deleted file mode 100644 index d17c86f96..000000000 --- a/org.argeo.enterprise/src/org/argeo/util/DictionaryKeys.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.argeo.util; - -import java.util.Dictionary; -import java.util.Enumeration; -import java.util.Iterator; - -/** - * Access the keys of a {@link String}-keyed {@link Dictionary} (common throughout - * the OSGi APIs) as an {@link Iterable} so that they are easily usable in - * for-each loops. - */ -class DictionaryKeys implements Iterable { - private final Dictionary dictionary; - - public DictionaryKeys(Dictionary dictionary) { - this.dictionary = dictionary; - } - - @Override - public Iterator iterator() { - return new KeyIterator(dictionary.keys()); - } - - private static class KeyIterator implements Iterator { - private final Enumeration keys; - - KeyIterator(Enumeration keys) { - this.keys = keys; - } - - @Override - public boolean hasNext() { - return keys.hasMoreElements(); - } - - @Override - public String next() { - return keys.nextElement(); - } - - } -} diff --git a/org.argeo.enterprise/src/org/argeo/util/DigestUtils.java b/org.argeo.enterprise/src/org/argeo/util/DigestUtils.java deleted file mode 100644 index ce018007c..000000000 --- a/org.argeo.enterprise/src/org/argeo/util/DigestUtils.java +++ /dev/null @@ -1,201 +0,0 @@ -package org.argeo.util; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.channels.FileChannel.MapMode; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -/** Utilities around cryptographic digests */ -public class DigestUtils { - public final static String MD5 = "MD5"; - public final static String SHA1 = "SHA1"; - public final static String SHA256 = "SHA-256"; - public final static String SHA512 = "SHA-512"; - - private static Boolean debug = false; - // TODO: make it configurable - private final static Integer byteBufferCapacity = 100 * 1024;// 100 KB - - public static byte[] sha1(byte[] bytes) { - try { - MessageDigest digest = MessageDigest.getInstance(SHA1); - digest.update(bytes); - byte[] checksum = digest.digest(); - return checksum; - } catch (NoSuchAlgorithmException e) { - throw new IllegalArgumentException(e); - } - } - - public static String digest(String algorithm, byte[] bytes) { - try { - MessageDigest digest = MessageDigest.getInstance(algorithm); - digest.update(bytes); - byte[] checksum = digest.digest(); - String res = encodeHexString(checksum); - return res; - } catch (NoSuchAlgorithmException e) { - throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e); - } - } - - public static String digest(String algorithm, InputStream in) { - try { - MessageDigest digest = MessageDigest.getInstance(algorithm); - // ReadableByteChannel channel = Channels.newChannel(in); - // ByteBuffer bb = ByteBuffer.allocateDirect(byteBufferCapacity); - // while (channel.read(bb) > 0) - // digest.update(bb); - byte[] buffer = new byte[byteBufferCapacity]; - int read = 0; - while ((read = in.read(buffer)) > 0) { - digest.update(buffer, 0, read); - } - - byte[] checksum = digest.digest(); - String res = encodeHexString(checksum); - return res; - } catch (NoSuchAlgorithmException e) { - throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e); - } catch (IOException e) { - throw new RuntimeException(e); - } finally { - StreamUtils.closeQuietly(in); - } - } - - public static String digest(String algorithm, File file) { - FileInputStream fis = null; - FileChannel fc = null; - try { - fis = new FileInputStream(file); - fc = fis.getChannel(); - - // Get the file's size and then map it into memory - int sz = (int) fc.size(); - ByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, sz); - return digest(algorithm, bb); - } catch (IOException e) { - throw new IllegalArgumentException("Cannot digest " + file + " with algorithm " + algorithm, e); - } finally { - StreamUtils.closeQuietly(fis); - if (fc.isOpen()) - try { - fc.close(); - } catch (IOException e) { - // silent - } - } - } - - protected static String digest(String algorithm, ByteBuffer bb) { - long begin = System.currentTimeMillis(); - try { - MessageDigest digest = MessageDigest.getInstance(algorithm); - digest.update(bb); - byte[] checksum = digest.digest(); - String res = encodeHexString(checksum); - long end = System.currentTimeMillis(); - if (debug) - System.out.println((end - begin) + " ms / " + ((end - begin) / 1000) + " s"); - return res; - } catch (NoSuchAlgorithmException e) { - throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e); - } - } - - public static String sha1hex(Path path) { - return digest(SHA1, path, byteBufferCapacity); - } - - public static String digest(String algorithm, Path path, long bufferSize) { - byte[] digest = digestRaw(algorithm, path, bufferSize); - return encodeHexString(digest); - } - - public static byte[] digestRaw(String algorithm, Path file, long bufferSize) { - long begin = System.currentTimeMillis(); - try { - MessageDigest md = MessageDigest.getInstance(algorithm); - FileChannel fc = FileChannel.open(file); - long fileSize = Files.size(file); - if (fileSize <= bufferSize) { - ByteBuffer bb = fc.map(MapMode.READ_ONLY, 0, fileSize); - md.update(bb); - } else { - long lastCycle = (fileSize / bufferSize) - 1; - long position = 0; - for (int i = 0; i <= lastCycle; i++) { - ByteBuffer bb; - if (i != lastCycle) { - bb = fc.map(MapMode.READ_ONLY, position, bufferSize); - position = position + bufferSize; - } else { - bb = fc.map(MapMode.READ_ONLY, position, fileSize - position); - position = fileSize; - } - md.update(bb); - } - } - long end = System.currentTimeMillis(); - if (debug) - System.out.println((end - begin) + " ms / " + ((end - begin) / 1000) + " s"); - return md.digest(); - } catch (NoSuchAlgorithmException e) { - throw new IllegalArgumentException("Cannot digest " + file + " with algorithm " + algorithm, e); - } catch (IOException e) { - throw new RuntimeException("Cannot digest " + file + " with algorithm " + algorithm, e); - } - } - - public static void main(String[] args) { - File file; - if (args.length > 0) - file = new File(args[0]); - else { - System.err.println("Usage: []" + " (see http://java.sun.com/j2se/1.5.0/" - + "docs/guide/security/CryptoSpec.html#AppA)"); - return; - } - - if (args.length > 1) { - String algorithm = args[1]; - System.out.println(digest(algorithm, file)); - } else { - String algorithm = "MD5"; - System.out.println(algorithm + ": " + digest(algorithm, file)); - algorithm = "SHA"; - System.out.println(algorithm + ": " + digest(algorithm, file)); - System.out.println(algorithm + ": " + sha1hex(file.toPath())); - algorithm = "SHA-256"; - System.out.println(algorithm + ": " + digest(algorithm, file)); - algorithm = "SHA-512"; - System.out.println(algorithm + ": " + digest(algorithm, file)); - } - } - - final private static char[] hexArray = "0123456789abcdef".toCharArray(); - - /** - * From - * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to - * -a-hex-string-in-java - */ - public static String encodeHexString(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = hexArray[v >>> 4]; - hexChars[j * 2 + 1] = hexArray[v & 0x0F]; - } - return new String(hexChars); - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/util/DirH.java b/org.argeo.enterprise/src/org/argeo/util/DirH.java deleted file mode 100644 index b6d962f06..000000000 --- a/org.argeo.enterprise/src/org/argeo/util/DirH.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.argeo.util; - -import java.io.IOException; -import java.io.PrintStream; -import java.nio.charset.Charset; -import java.nio.file.DirectoryStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** Hashes the hashes of the files in a directory. */ -public class DirH { - - private final static Charset charset = Charset.forName("UTF-16"); - private final static long bufferSize = 200 * 1024 * 1024; - private final static String algorithm = "SHA"; - - private final static byte EOL = (byte) '\n'; - private final static byte SPACE = (byte) ' '; - - private final int hashSize; - - private final byte[][] hashes; - private final byte[][] fileNames; - private final byte[] digest; - private final byte[] dirName; - - /** - * @param dirName can be null or empty - */ - private DirH(byte[][] hashes, byte[][] fileNames, byte[] dirName) { - if (hashes.length != fileNames.length) - throw new IllegalArgumentException(hashes.length + " hashes and " + fileNames.length + " file names"); - this.hashes = hashes; - this.fileNames = fileNames; - this.dirName = dirName == null ? new byte[0] : dirName; - if (hashes.length == 0) {// empty dir - hashSize = 20; - // FIXME what is the digest of an empty dir? - digest = new byte[hashSize]; - Arrays.fill(digest, SPACE); - return; - } - hashSize = hashes[0].length; - for (int i = 0; i < hashes.length; i++) { - if (hashes[i].length != hashSize) - throw new IllegalArgumentException( - "Hash size for " + new String(fileNames[i], charset) + " is " + hashes[i].length); - } - - try { - MessageDigest md = MessageDigest.getInstance(algorithm); - for (int i = 0; i < hashes.length; i++) { - md.update(this.hashes[i]); - md.update(SPACE); - md.update(this.fileNames[i]); - md.update(EOL); - } - digest = md.digest(); - } catch (NoSuchAlgorithmException e) { - throw new IllegalArgumentException("Cannot digest", e); - } - } - - public void print(PrintStream out) { - out.print(DigestUtils.encodeHexString(digest)); - if (dirName.length > 0) { - out.print(' '); - out.print(new String(dirName, charset)); - } - out.print('\n'); - for (int i = 0; i < hashes.length; i++) { - out.print(DigestUtils.encodeHexString(hashes[i])); - out.print(' '); - out.print(new String(fileNames[i], charset)); - out.print('\n'); - } - } - - public static DirH digest(Path dir) { - try (DirectoryStream files = Files.newDirectoryStream(dir)) { - List hs = new ArrayList(); - List fNames = new ArrayList<>(); - for (Path file : files) { - if (!Files.isDirectory(file)) { - byte[] digest = DigestUtils.digestRaw(algorithm, file, bufferSize); - hs.add(digest); - fNames.add(file.getFileName().toString()); - } - } - - byte[][] fileNames = new byte[fNames.size()][]; - for (int i = 0; i < fNames.size(); i++) { - fileNames[i] = fNames.get(i).getBytes(charset); - } - byte[][] hashes = hs.toArray(new byte[hs.size()][]); - return new DirH(hashes, fileNames, dir.toString().getBytes(charset)); - } catch (IOException e) { - throw new RuntimeException("Cannot digest " + dir, e); - } - } - - public static void main(String[] args) { - try { - DirH dirH = DirH.digest(Paths.get("/home/mbaudier/tmp/")); - dirH.print(System.out); - } catch (Exception e) { - e.printStackTrace(); - } - } -} diff --git a/org.argeo.enterprise/src/org/argeo/util/LangUtils.java b/org.argeo.enterprise/src/org/argeo/util/LangUtils.java deleted file mode 100644 index 162294537..000000000 --- a/org.argeo.enterprise/src/org/argeo/util/LangUtils.java +++ /dev/null @@ -1,284 +0,0 @@ -package org.argeo.util; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Writer; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.StandardOpenOption; -import java.time.ZonedDateTime; -import java.time.temporal.ChronoUnit; -import java.time.temporal.Temporal; -import java.util.ArrayList; -import java.util.Dictionary; -import java.util.Enumeration; -import java.util.HashMap; -import java.util.Hashtable; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; - -/** Utilities around Java basic features. */ -public class LangUtils { - /* - * NON-API OSGi - */ - /** - * Returns an array with the names of the provided classes. Useful when - * registering services with multiple interfaces in OSGi. - */ - public static String[] names(Class... clzz) { - String[] res = new String[clzz.length]; - for (int i = 0; i < clzz.length; i++) - res[i] = clzz[i].getName(); - return res; - } - - /* - * MAP - */ - /** - * Creates a new {@link Dictionary} with one key-value pair. Key should not be - * null, but if the value is null, it returns an empty {@link Dictionary}. - */ - public static Map map(String key, Object value) { - assert key != null; - HashMap props = new HashMap<>(); - if (value != null) - props.put(key, value); - return props; - } - - /* - * DICTIONARY - */ - - /** - * Creates a new {@link Dictionary} with one key-value pair. Key should not be - * null, but if the value is null, it returns an empty {@link Dictionary}. - */ - public static Dictionary dict(String key, Object value) { - assert key != null; - Hashtable props = new Hashtable<>(); - if (value != null) - props.put(key, value); - return props; - } - - /** @deprecated Use {@link #dict(String, Object)} instead. */ - @Deprecated - public static Dictionary dico(String key, Object value) { - return dict(key, value); - } - - /** Converts a {@link Dictionary} to a {@link Map} of strings. */ - public static Map dictToStringMap(Dictionary properties) { - if (properties == null) { - return null; - } - Map res = new HashMap<>(properties.size()); - Enumeration keys = properties.keys(); - while (keys.hasMoreElements()) { - String key = keys.nextElement(); - res.put(key, properties.get(key).toString()); - } - return res; - } - - /** - * Get a string property from this map, expecting to find it, or - * null if not found. - */ - public static String get(Map map, String key) { - Object res = map.get(key); - if (res == null) - return null; - return res.toString(); - } - - /** - * Get a string property from this map, expecting to find it. - * - * @throws IllegalArgumentException if the key was not found - */ - public static String getNotNull(Map map, String key) { - Object res = map.get(key); - if (res == null) - throw new IllegalArgumentException("Map " + map + " should contain key " + key); - return res.toString(); - } - - /** - * Wraps the keys of the provided {@link Dictionary} as an {@link Iterable}. - */ - public static Iterable keys(Dictionary props) { - assert props != null; - return new DictionaryKeys(props); - } - - static String toJson(Dictionary props) { - return toJson(props, false); - } - - static String toJson(Dictionary props, boolean pretty) { - StringBuilder sb = new StringBuilder(); - sb.append('{'); - if (pretty) - sb.append('\n'); - Enumeration keys = props.keys(); - while (keys.hasMoreElements()) { - String key = keys.nextElement(); - if (pretty) - sb.append(' '); - sb.append('\"').append(key).append('\"'); - if (pretty) - sb.append(" : "); - else - sb.append(':'); - sb.append('\"').append(props.get(key)).append('\"'); - if (keys.hasMoreElements()) - sb.append(", "); - if (pretty) - sb.append('\n'); - } - sb.append('}'); - return sb.toString(); - } - - static void storeAsProperties(Dictionary props, Path path) throws IOException { - if (props == null) - throw new IllegalArgumentException("Props cannot be null"); - Properties toStore = new Properties(); - for (Enumeration keys = props.keys(); keys.hasMoreElements();) { - String key = keys.nextElement(); - toStore.setProperty(key, props.get(key).toString()); - } - try (OutputStream out = Files.newOutputStream(path)) { - toStore.store(out, null); - } - } - - static void appendAsLdif(String dnBase, String dnKey, Dictionary props, Path path) - throws IOException { - if (props == null) - throw new IllegalArgumentException("Props cannot be null"); - Object dnValue = props.get(dnKey); - String dnStr = dnKey + '=' + dnValue + ',' + dnBase; - LdapName dn; - try { - dn = new LdapName(dnStr); - } catch (InvalidNameException e) { - throw new IllegalArgumentException("Cannot interpret DN " + dnStr, e); - } - if (dnValue == null) - throw new IllegalArgumentException("DN key " + dnKey + " must have a value"); - try (Writer writer = Files.newBufferedWriter(path, StandardOpenOption.APPEND, StandardOpenOption.CREATE)) { - writer.append("\ndn: "); - writer.append(dn.toString()); - writer.append('\n'); - for (Enumeration keys = props.keys(); keys.hasMoreElements();) { - String key = keys.nextElement(); - Object value = props.get(key); - writer.append(key); - writer.append(": "); - // FIXME deal with binary and multiple values - writer.append(value.toString()); - writer.append('\n'); - } - } - } - - static Dictionary loadFromProperties(Path path) throws IOException { - Properties toLoad = new Properties(); - try (InputStream in = Files.newInputStream(path)) { - toLoad.load(in); - } - Dictionary res = new Hashtable(); - for (Object key : toLoad.keySet()) - res.put(key.toString(), toLoad.get(key)); - return res; - } - - /* - * COLLECTIONS - */ - /** - * Convert a comma-separated separated {@link String} or a {@link String} array - * to a {@link List} of {@link String}, trimming them. Useful to quickly - * interpret OSGi services properties. - * - * @return a {@link List} containing the trimmed {@link String}s, or an empty - * {@link List} if the argument was null. - */ - public static List toStringList(Object value) { - List values = new ArrayList<>(); - if (value == null) - return values; - String[] arr; - if (value instanceof String) { - arr = ((String) value).split(","); - } else if (value instanceof String[]) { - arr = (String[]) value; - } else { - throw new IllegalArgumentException("Unsupported value type " + value.getClass()); - } - for (String str : arr) { - values.add(str.trim()); - } - return values; - } - - /* - * EXCEPTIONS - */ - /** - * Chain the messages of all causes (one per line, starts with a line - * return) without all the stack - */ - public static String chainCausesMessages(Throwable t) { - StringBuffer buf = new StringBuffer(); - chainCauseMessage(buf, t); - return buf.toString(); - } - - /** Recursive chaining of messages */ - private static void chainCauseMessage(StringBuffer buf, Throwable t) { - buf.append('\n').append(' ').append(t.getClass().getCanonicalName()).append(": ").append(t.getMessage()); - if (t.getCause() != null) - chainCauseMessage(buf, t.getCause()); - } - - /* - * TIME - */ - /** Formats time elapsed since start. */ - public static String since(ZonedDateTime start) { - ZonedDateTime now = ZonedDateTime.now(); - return duration(start, now); - } - - /** Formats a duration. */ - public static String duration(Temporal start, Temporal end) { - long count = ChronoUnit.DAYS.between(start, end); - if (count != 0) - return count > 1 ? count + " days" : count + " day"; - count = ChronoUnit.HOURS.between(start, end); - if (count != 0) - return count > 1 ? count + " hours" : count + " hours"; - count = ChronoUnit.MINUTES.between(start, end); - if (count != 0) - return count > 1 ? count + " minutes" : count + " minute"; - count = ChronoUnit.SECONDS.between(start, end); - return count > 1 ? count + " seconds" : count + " second"; - } - - /** Singleton constructor. */ - private LangUtils() { - - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/util/OS.java b/org.argeo.enterprise/src/org/argeo/util/OS.java deleted file mode 100644 index d8127b600..000000000 --- a/org.argeo.enterprise/src/org/argeo/util/OS.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.argeo.util; - -import java.io.File; -import java.lang.management.ManagementFactory; - -/** When OS specific informations are needed. */ -public class OS { - public final static OS LOCAL = new OS(); - - private final String arch, name, version; - - /** The OS of the running JVM */ - protected OS() { - arch = System.getProperty("os.arch"); - name = System.getProperty("os.name"); - version = System.getProperty("os.version"); - } - - public String getArch() { - return arch; - } - - public String getName() { - return name; - } - - public String getVersion() { - return version; - } - - public boolean isMSWindows() { - // only MS Windows would use such an horrendous separator... - return File.separatorChar == '\\'; - } - - public String[] getDefaultShellCommand() { - if (!isMSWindows()) - return new String[] { "/bin/sh", "-l", "-i" }; - else - return new String[] { "cmd.exe", "/C" }; - } - - public static Integer getJvmPid() { - /* - * This method works on most platforms (including Linux). Although when Java 9 - * comes along, there is a better way: long pid = - * ProcessHandle.current().getPid(); - * - * See: - * http://stackoverflow.com/questions/35842/how-can-a-java-program-get-its-own- - * process-id - */ - String pidAndHost = ManagementFactory.getRuntimeMXBean().getName(); - return Integer.parseInt(pidAndHost.substring(0, pidAndHost.indexOf('@'))); - } -} diff --git a/org.argeo.enterprise/src/org/argeo/util/PasswordEncryption.java b/org.argeo.enterprise/src/org/argeo/util/PasswordEncryption.java deleted file mode 100644 index c95c7879e..000000000 --- a/org.argeo.enterprise/src/org/argeo/util/PasswordEncryption.java +++ /dev/null @@ -1,216 +0,0 @@ -package org.argeo.util; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.security.GeneralSecurityException; -import java.security.InvalidKeyException; -import java.security.Key; - -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.SecretKeySpec; - -public class PasswordEncryption { - public final static Integer DEFAULT_ITERATION_COUNT = 1024; - /** Stronger with 256, but causes problem with Oracle JVM */ - public final static Integer DEFAULT_SECRETE_KEY_LENGTH = 256; - public final static Integer DEFAULT_SECRETE_KEY_LENGTH_RESTRICTED = 128; - public final static String DEFAULT_SECRETE_KEY_FACTORY = "PBKDF2WithHmacSHA1"; - public final static String DEFAULT_SECRETE_KEY_ENCRYPTION = "AES"; - public final static String DEFAULT_CIPHER_NAME = "AES/CBC/PKCS5Padding"; -// public final static String DEFAULT_CHARSET = "UTF-8"; - public final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - - private Integer iterationCount = DEFAULT_ITERATION_COUNT; - private Integer secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH; - private String secreteKeyFactoryName = DEFAULT_SECRETE_KEY_FACTORY; - private String secreteKeyEncryption = DEFAULT_SECRETE_KEY_ENCRYPTION; - private String cipherName = DEFAULT_CIPHER_NAME; - - private static byte[] DEFAULT_SALT_8 = { (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56, - (byte) 0x35, (byte) 0xE3, (byte) 0x03 }; - private static byte[] DEFAULT_IV_16 = { (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56, - (byte) 0x35, (byte) 0xE3, (byte) 0x03, (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56, - (byte) 0x35, (byte) 0xE3, (byte) 0x03 }; - - private Key key; - private Cipher ecipher; - private Cipher dcipher; - - private String securityProviderName = null; - - /** - * This is up to the caller to clear the passed array. Neither copy of nor - * reference to the passed array is kept - */ - public PasswordEncryption(char[] password) { - this(password, DEFAULT_SALT_8, DEFAULT_IV_16); - } - - /** - * This is up to the caller to clear the passed array. Neither copies of nor - * references to the passed arrays are kept - */ - public PasswordEncryption(char[] password, byte[] passwordSalt, byte[] initializationVector) { - try { - initKeyAndCiphers(password, passwordSalt, initializationVector); - } catch (InvalidKeyException e) { - Integer previousSecreteKeyLength = secreteKeyLength; - secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH_RESTRICTED; - System.err.println("'" + e.getMessage() + "', will use " + secreteKeyLength - + " secrete key length instead of " + previousSecreteKeyLength); - try { - initKeyAndCiphers(password, passwordSalt, initializationVector); - } catch (GeneralSecurityException e1) { - throw new IllegalStateException("Cannot get secret key (with restricted length)", e1); - } - } catch (GeneralSecurityException e) { - throw new IllegalStateException("Cannot get secret key", e); - } - } - - protected void initKeyAndCiphers(char[] password, byte[] passwordSalt, byte[] initializationVector) - throws GeneralSecurityException { - byte[] salt = new byte[8]; - System.arraycopy(passwordSalt, 0, salt, 0, salt.length); - // for (int i = 0; i < password.length && i < salt.length; i++) - // salt[i] = (byte) password[i]; - byte[] iv = new byte[16]; - System.arraycopy(initializationVector, 0, iv, 0, iv.length); - - SecretKeyFactory keyFac = SecretKeyFactory.getInstance(getSecretKeyFactoryName()); - PBEKeySpec keySpec = new PBEKeySpec(password, salt, getIterationCount(), getKeyLength()); - String secKeyEncryption = getSecretKeyEncryption(); - if (secKeyEncryption != null) { - SecretKey tmp = keyFac.generateSecret(keySpec); - key = new SecretKeySpec(tmp.getEncoded(), getSecretKeyEncryption()); - } else { - key = keyFac.generateSecret(keySpec); - } - if (securityProviderName != null) - ecipher = Cipher.getInstance(getCipherName(), securityProviderName); - else - ecipher = Cipher.getInstance(getCipherName()); - ecipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv)); - dcipher = Cipher.getInstance(getCipherName()); - dcipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv)); - } - - public void encrypt(InputStream decryptedIn, OutputStream encryptedOut) throws IOException { - try { - CipherOutputStream out = new CipherOutputStream(encryptedOut, ecipher); - StreamUtils.copy(decryptedIn, out); - StreamUtils.closeQuietly(out); - } catch (IOException e) { - throw e; - } finally { - StreamUtils.closeQuietly(decryptedIn); - } - } - - public void decrypt(InputStream encryptedIn, OutputStream decryptedOut) throws IOException { - try { - CipherInputStream decryptedIn = new CipherInputStream(encryptedIn, dcipher); - StreamUtils.copy(decryptedIn, decryptedOut); - } catch (IOException e) { - throw e; - } finally { - StreamUtils.closeQuietly(encryptedIn); - } - } - - public byte[] encryptString(String str) { - ByteArrayOutputStream out = null; - ByteArrayInputStream in = null; - try { - out = new ByteArrayOutputStream(); - in = new ByteArrayInputStream(str.getBytes(DEFAULT_CHARSET)); - encrypt(in, out); - return out.toByteArray(); - } catch (IOException e) { - throw new RuntimeException(e); - } finally { - StreamUtils.closeQuietly(out); - } - } - - /** Closes the input stream */ - public String decryptAsString(InputStream in) { - ByteArrayOutputStream out = null; - try { - out = new ByteArrayOutputStream(); - decrypt(in, out); - return new String(out.toByteArray(), DEFAULT_CHARSET); - } catch (IOException e) { - throw new RuntimeException(e); - } finally { - StreamUtils.closeQuietly(out); - } - } - - protected Key getKey() { - return key; - } - - protected Cipher getEcipher() { - return ecipher; - } - - protected Cipher getDcipher() { - return dcipher; - } - - protected Integer getIterationCount() { - return iterationCount; - } - - protected Integer getKeyLength() { - return secreteKeyLength; - } - - protected String getSecretKeyFactoryName() { - return secreteKeyFactoryName; - } - - protected String getSecretKeyEncryption() { - return secreteKeyEncryption; - } - - protected String getCipherName() { - return cipherName; - } - - public void setIterationCount(Integer iterationCount) { - this.iterationCount = iterationCount; - } - - public void setSecreteKeyLength(Integer keyLength) { - this.secreteKeyLength = keyLength; - } - - public void setSecreteKeyFactoryName(String secreteKeyFactoryName) { - this.secreteKeyFactoryName = secreteKeyFactoryName; - } - - public void setSecreteKeyEncryption(String secreteKeyEncryption) { - this.secreteKeyEncryption = secreteKeyEncryption; - } - - public void setCipherName(String cipherName) { - this.cipherName = cipherName; - } - - public void setSecurityProviderName(String securityProviderName) { - this.securityProviderName = securityProviderName; - } -} diff --git a/org.argeo.enterprise/src/org/argeo/util/ServiceChannel.java b/org.argeo.enterprise/src/org/argeo/util/ServiceChannel.java deleted file mode 100644 index 799738414..000000000 --- a/org.argeo.enterprise/src/org/argeo/util/ServiceChannel.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.argeo.util; - -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.channels.AsynchronousByteChannel; -import java.nio.channels.CompletionHandler; -import java.nio.channels.ReadableByteChannel; -import java.nio.channels.WritableByteChannel; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; - -/** An {@link AsynchronousByteChannel} based on an {@link ExecutorService}. */ -public class ServiceChannel implements AsynchronousByteChannel { - private final ReadableByteChannel in; - private final WritableByteChannel out; - - private boolean open = true; - - private ExecutorService executor; - - public ServiceChannel(ReadableByteChannel in, WritableByteChannel out, ExecutorService executor) { - this.in = in; - this.out = out; - this.executor = executor; - } - - @Override - public Future read(ByteBuffer dst) { - return executor.submit(() -> in.read(dst)); - } - - @Override - public void read(ByteBuffer dst, A attachment, CompletionHandler handler) { - try { - Future res = read(dst); - handler.completed(res.get(), attachment); - } catch (Exception e) { - handler.failed(e, attachment); - } - } - - @Override - public Future write(ByteBuffer src) { - return executor.submit(() -> out.write(src)); - } - - @Override - public void write(ByteBuffer src, A attachment, CompletionHandler handler) { - try { - Future res = write(src); - handler.completed(res.get(), attachment); - } catch (Exception e) { - handler.failed(e, attachment); - } - } - - @Override - public synchronized void close() throws IOException { - try { - in.close(); - } catch (Exception e) { - e.printStackTrace(); - } - try { - out.close(); - } catch (Exception e) { - e.printStackTrace(); - } - open = false; - notifyAll(); - } - - @Override - public synchronized boolean isOpen() { - return open; - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/util/StreamUtils.java b/org.argeo.enterprise/src/org/argeo/util/StreamUtils.java deleted file mode 100644 index 6d7d940ce..000000000 --- a/org.argeo.enterprise/src/org/argeo/util/StreamUtils.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.argeo.util; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Reader; -import java.io.Writer; - -/** Utilities to be used when Apache Commons IO is not available. */ -class StreamUtils { - private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; - - /* - * APACHE COMMONS IO (inspired) - */ - - /** @return the number of bytes */ - public static Long copy(InputStream in, OutputStream out) - throws IOException { - Long count = 0l; - byte[] buf = new byte[DEFAULT_BUFFER_SIZE]; - while (true) { - int length = in.read(buf); - if (length < 0) - break; - out.write(buf, 0, length); - count = count + length; - } - return count; - } - - /** @return the number of chars */ - public static Long copy(Reader in, Writer out) throws IOException { - Long count = 0l; - char[] buf = new char[DEFAULT_BUFFER_SIZE]; - while (true) { - int length = in.read(buf); - if (length < 0) - break; - out.write(buf, 0, length); - count = count + length; - } - return count; - } - - public static void closeQuietly(InputStream in) { - if (in != null) - try { - in.close(); - } catch (Exception e) { - // - } - } - - public static void closeQuietly(OutputStream out) { - if (out != null) - try { - out.close(); - } catch (Exception e) { - // - } - } - - public static void closeQuietly(Reader in) { - if (in != null) - try { - in.close(); - } catch (Exception e) { - // - } - } - - public static void closeQuietly(Writer out) { - if (out != null) - try { - out.close(); - } catch (Exception e) { - // - } - } -} diff --git a/org.argeo.enterprise/src/org/argeo/util/Tester.java b/org.argeo.enterprise/src/org/argeo/util/Tester.java deleted file mode 100644 index 31a2be4ec..000000000 --- a/org.argeo.enterprise/src/org/argeo/util/Tester.java +++ /dev/null @@ -1,126 +0,0 @@ -package org.argeo.util; - -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -/** A generic tester based on Java assertions and functional programming. */ -public class Tester { - private Map results = Collections.synchronizedSortedMap(new TreeMap<>()); - - private ClassLoader classLoader; - - /** Use {@link Thread#getContextClassLoader()} by default. */ - public Tester() { - this(Thread.currentThread().getContextClassLoader()); - } - - public Tester(ClassLoader classLoader) { - this.classLoader = classLoader; - } - - public void execute(String className) { - Class clss; - try { - clss = classLoader.loadClass(className); - boolean assertionsEnabled = clss.desiredAssertionStatus(); - if (!assertionsEnabled) - throw new IllegalStateException("Test runner " + getClass().getName() - + " requires Java assertions to be enabled. Call the JVM with the -ea argument."); - } catch (Exception e1) { - throw new IllegalArgumentException("Cannot initalise test for " + className, e1); - - } - List methods = findMethods(clss); - if (methods.size() == 0) - throw new IllegalArgumentException("No test method found in " + clss); - // TODO make order more predictable? - for (Method method : methods) { - String uid = method.getDeclaringClass().getName() + "#" + method.getName(); - TesterStatus testStatus = new TesterStatus(uid); - Object obj = null; - try { - beforeTest(uid, method); - obj = clss.getDeclaredConstructor().newInstance(); - method.invoke(obj); - testStatus.setPassed(); - afterTestPassed(uid, method, obj); - } catch (Exception e) { - testStatus.setFailed(e); - afterTestFailed(uid, method, obj, e); - } finally { - results.put(uid, testStatus); - } - } - } - - protected void beforeTest(String uid, Method method) { - // System.out.println(uid + ": STARTING"); - } - - protected void afterTestPassed(String uid, Method method, Object obj) { - System.out.println(uid + ": PASSED"); - } - - protected void afterTestFailed(String uid, Method method, Object obj, Throwable e) { - System.out.println(uid + ": FAILED"); - e.printStackTrace(); - } - - protected List findMethods(Class clss) { - List methods = new ArrayList(); -// Method call = getMethod(clss, "call"); -// if (call != null) -// methods.add(call); -// - for (Method method : clss.getMethods()) { - if (method.getName().startsWith("test")) { - methods.add(method); - } - } - return methods; - } - - protected Method getMethod(Class clss, String name, Class... parameterTypes) { - try { - return clss.getMethod(name, parameterTypes); - } catch (NoSuchMethodException e) { - return null; - } catch (SecurityException e) { - throw new IllegalStateException(e); - } - } - - public static void main(String[] args) { - // deal with arguments - String className; - if (args.length < 1) { - System.err.println(usage()); - System.exit(1); - throw new IllegalArgumentException(); - } else { - className = args[0]; - } - - Tester test = new Tester(); - try { - test.execute(className); - } catch (Throwable e) { - e.printStackTrace(); - } - - Map r = test.results; - for (String uid : r.keySet()) { - TesterStatus testStatus = r.get(uid); - System.out.println(testStatus); - } - } - - public static String usage() { - return "java " + Tester.class.getName() + " [test class name]"; - - } -} diff --git a/org.argeo.enterprise/src/org/argeo/util/TesterStatus.java b/org.argeo.enterprise/src/org/argeo/util/TesterStatus.java deleted file mode 100644 index d1d14ed06..000000000 --- a/org.argeo.enterprise/src/org/argeo/util/TesterStatus.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.argeo.util; - -import java.io.Serializable; - -/** The status of a test. */ -public class TesterStatus implements Serializable { - private static final long serialVersionUID = 6272975746885487000L; - - private Boolean passed = null; - private final String uid; - private Throwable throwable = null; - - public TesterStatus(String uid) { - this.uid = uid; - } - - /** For cloning. */ - public TesterStatus(String uid, Boolean passed, Throwable throwable) { - this(uid); - this.passed = passed; - this.throwable = throwable; - } - - public synchronized Boolean isRunning() { - return passed == null; - } - - public synchronized Boolean isPassed() { - assert passed != null; - return passed; - } - - public synchronized Boolean isFailed() { - assert passed != null; - return !passed; - } - - public synchronized void setPassed() { - setStatus(true); - } - - public synchronized void setFailed() { - setStatus(false); - } - - public synchronized void setFailed(Throwable throwable) { - setStatus(false); - setThrowable(throwable); - } - - protected void setStatus(Boolean passed) { - if (this.passed != null) - throw new IllegalStateException("Passed status of test " + uid + " is already set (to " + passed + ")"); - this.passed = passed; - } - - protected void setThrowable(Throwable throwable) { - if (this.throwable != null) - throw new IllegalStateException("Throwable of test " + uid + " is already set (to " + passed + ")"); - this.throwable = throwable; - } - - public String getUid() { - return uid; - } - - public Throwable getThrowable() { - return throwable; - } - - @Override - protected Object clone() throws CloneNotSupportedException { - // TODO Auto-generated method stub - return super.clone(); - } - - @Override - public boolean equals(Object o) { - if (o instanceof TesterStatus) { - TesterStatus other = (TesterStatus) o; - // we don't check consistency for performance purposes - // this equals() is supposed to be used in collections or for transfer - return other.uid.equals(uid); - } - return false; - } - - @Override - public int hashCode() { - return uid.hashCode(); - } - - @Override - public String toString() { - return uid + "\t" + (passed ? "passed" : "failed"); - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/util/Throughput.java b/org.argeo.enterprise/src/org/argeo/util/Throughput.java deleted file mode 100644 index 266ddbc58..000000000 --- a/org.argeo.enterprise/src/org/argeo/util/Throughput.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.argeo.util; - -import java.text.NumberFormat; -import java.text.ParseException; -import java.util.Locale; - -/** A throughput, that is, a value per unit of time. */ -public class Throughput { - private final static NumberFormat usNumberFormat = NumberFormat.getInstance(Locale.US); - - public enum Unit { - s, m, h, d - } - - private final Double value; - private final Unit unit; - - public Throughput(Double value, Unit unit) { - this.value = value; - this.unit = unit; - } - - public Throughput(Long periodMs, Long count, Unit unit) { - if (unit.equals(Unit.s)) - value = ((double) count * 1000d) / periodMs; - else if (unit.equals(Unit.m)) - value = ((double) count * 60d * 1000d) / periodMs; - else if (unit.equals(Unit.h)) - value = ((double) count * 60d * 60d * 1000d) / periodMs; - else if (unit.equals(Unit.d)) - value = ((double) count * 24d * 60d * 60d * 1000d) / periodMs; - else - throw new IllegalArgumentException("Unsupported unit " + unit); - this.unit = unit; - } - - public Throughput(Double value, String unitStr) { - this(value, Unit.valueOf(unitStr)); - } - - public Throughput(String def) { - int index = def.indexOf('/'); - if (def.length() < 3 || index <= 0 || index != def.length() - 2) - throw new IllegalArgumentException( - def + " no a proper throughput definition" + " (should be /, e.g. 3.54/s or 1500/h"); - String valueStr = def.substring(0, index); - String unitStr = def.substring(index + 1); - try { - this.value = usNumberFormat.parse(valueStr).doubleValue(); - } catch (ParseException e) { - throw new IllegalArgumentException("Cannot parse " + valueStr + " as a number.", e); - } - this.unit = Unit.valueOf(unitStr); - } - - public Long asMsPeriod() { - if (unit.equals(Unit.s)) - return Math.round(1000d / value); - else if (unit.equals(Unit.m)) - return Math.round((60d * 1000d) / value); - else if (unit.equals(Unit.h)) - return Math.round((60d * 60d * 1000d) / value); - else if (unit.equals(Unit.d)) - return Math.round((24d * 60d * 60d * 1000d) / value); - else - throw new IllegalArgumentException("Unsupported unit " + unit); - } - - @Override - public String toString() { - return usNumberFormat.format(value) + '/' + unit; - } - - public Double getValue() { - return value; - } - - public Unit getUnit() { - return unit; - } - -} diff --git a/org.argeo.enterprise/src/org/argeo/util/UuidUtils.java b/org.argeo.enterprise/src/org/argeo/util/UuidUtils.java deleted file mode 100644 index 7584abc1b..000000000 --- a/org.argeo.enterprise/src/org/argeo/util/UuidUtils.java +++ /dev/null @@ -1,378 +0,0 @@ -package org.argeo.util; - -import java.net.InetAddress; -import java.net.NetworkInterface; -import java.net.SocketException; -import java.net.UnknownHostException; -import java.security.SecureRandom; -import java.time.Duration; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.BitSet; -import java.util.Random; -import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; - -/** - * Utilities to simplify and extends usage of {@link UUID}. Only the RFC 4122 - * variant (also known as Leach–Salz variant) is supported. - */ -public class UuidUtils { - /** Nil UUID (00000000-0000-0000-0000-000000000000). */ - public final static UUID NIL_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000"); - public final static LocalDateTime GREGORIAN_START = LocalDateTime.of(1582, 10, 15, 0, 0, 0); - - private final static long MOST_SIG_VERSION1 = (1l << 12); - private final static long LEAST_SIG_RFC4122_VARIANT = (1l << 63); - - private final static SecureRandom RANDOM; - private final static AtomicInteger CLOCK_SEQUENCE; - private final static byte[] HARDWARE_ADDRESS; - /** A start timestamp to which {@link System#nanoTime()}/100 can be added. */ - private final static long START_TIMESTAMP; - static { - RANDOM = new SecureRandom(); - CLOCK_SEQUENCE = new AtomicInteger(RANDOM.nextInt(16384)); - HARDWARE_ADDRESS = getHardwareAddress(); - - long nowVm = System.nanoTime() / 100; - Duration duration = Duration.between(GREGORIAN_START, LocalDateTime.now(ZoneOffset.UTC)); - START_TIMESTAMP = (duration.getSeconds() * 10000000 + duration.getNano() / 100) - nowVm; - } - - private static byte[] getHardwareAddress() { - InetAddress localHost; - try { - localHost = InetAddress.getLocalHost(); - try { - NetworkInterface nic = NetworkInterface.getByInetAddress(localHost); - return nic.getHardwareAddress(); - } catch (SocketException e) { - return null; - } - } catch (UnknownHostException e) { - return null; - } - - } - - public static UUID timeUUIDwithRandomNode() { - long timestamp = START_TIMESTAMP + System.nanoTime() / 100; - return timeUUID(timestamp, RANDOM); - } - - public static UUID timeUUID(long timestamp, Random random) { - byte[] node = new byte[6]; - random.nextBytes(node); - node[0] = (byte) (node[0] | 1); - long clockSequence = CLOCK_SEQUENCE.incrementAndGet(); - return timeUUID(timestamp, clockSequence, node); - } - - public static UUID timeUUID() { - long timestamp = START_TIMESTAMP + System.nanoTime() / 100; - return timeUUID(timestamp); - } - - public static UUID timeUUID(long timestamp) { - if (HARDWARE_ADDRESS == null) - return timeUUID(timestamp, RANDOM); - long clockSequence = CLOCK_SEQUENCE.incrementAndGet(); - return timeUUID(timestamp, clockSequence, HARDWARE_ADDRESS); - } - - public static UUID timeUUID(long timestamp, NetworkInterface nic) { - byte[] node; - try { - node = nic.getHardwareAddress(); - } catch (SocketException e) { - throw new IllegalStateException("Cannot get hardware address", e); - } - long clockSequence = CLOCK_SEQUENCE.incrementAndGet(); - return timeUUID(timestamp, clockSequence, node); - } - - public static UUID timeUUID(LocalDateTime time, long clockSequence, byte[] node) { - Duration duration = Duration.between(GREGORIAN_START, time); - // Number of 100 ns intervals in one second: 1000000000 / 100 = 10000000 - long timestamp = duration.getSeconds() * 10000000 + duration.getNano() / 100; - return timeUUID(timestamp, clockSequence, node); - } - - public static UUID timeUUID(long timestamp, long clockSequence, byte[] node) { - assert node.length >= 6; - - long mostSig = MOST_SIG_VERSION1 // base for version 1 UUID - | ((timestamp & 0xFFFFFFFFL) << 32) // time_low - | (((timestamp >> 32) & 0xFFFFL) << 16) // time_mid - | ((timestamp >> 48) & 0x0FFFL);// time_hi_and_version - - long leastSig = LEAST_SIG_RFC4122_VARIANT // base for Leach–Salz UUID - | (((clockSequence & 0x3F00) >> 8) << 56) // clk_seq_hi_res - | ((clockSequence & 0xFF) << 48) // clk_seq_low - | (node[0] & 0xFFL) // - | ((node[1] & 0xFFL) << 8) // - | ((node[2] & 0xFFL) << 16) // - | ((node[3] & 0xFFL) << 24) // - | ((node[4] & 0xFFL) << 32) // - | ((node[5] & 0xFFL) << 40); // -// for (int i = 0; i < 6; i++) { -// leastSig = leastSig | ((node[i] & 0xFFL) << (8 * i)); -// } - UUID uuid = new UUID(mostSig, leastSig); - - // tests - assert uuid.node() == BitSet.valueOf(node).toLongArray()[0]; - assert uuid.timestamp() == timestamp; - assert uuid.clockSequence() == clockSequence; - assert uuid.version() == 1; - assert uuid.variant() == 2; - return uuid; - } - - @Deprecated - public static UUID timeBasedUUID() { - return timeBasedUUID(LocalDateTime.now(ZoneOffset.UTC)); - } - - @Deprecated - public static UUID timeBasedRandomUUID() { - return timeBasedRandomUUID(LocalDateTime.now(ZoneOffset.UTC), RANDOM); - } - - @Deprecated - public static UUID timeBasedUUID(LocalDateTime time) { - if (HARDWARE_ADDRESS == null) - return timeBasedRandomUUID(time, RANDOM); - return timeBasedUUID(time, BitSet.valueOf(HARDWARE_ADDRESS)); - } - - @Deprecated - public static UUID timeBasedAddressUUID(LocalDateTime time, NetworkInterface nic) throws SocketException { - byte[] nodeBytes = nic.getHardwareAddress(); - BitSet node = BitSet.valueOf(nodeBytes); - return timeBasedUUID(time, node); - } - - @Deprecated - public static UUID timeBasedRandomUUID(LocalDateTime time, Random random) { - byte[] nodeBytes = new byte[6]; - random.nextBytes(nodeBytes); - BitSet node = BitSet.valueOf(nodeBytes); - // set random marker - node.set(0, true); - return timeBasedUUID(time, node); - } - - @Deprecated - public static UUID timeBasedUUID(LocalDateTime time, BitSet node) { - // most significant - Duration duration = Duration.between(GREGORIAN_START, time); - - // Number of 100 ns intervals in one second: 1000000000 / 100 = 10000000 - long timeNanos = duration.getSeconds() * 10000000 + duration.getNano() / 100; - BitSet timeBits = BitSet.valueOf(new long[] { timeNanos }); - assert timeBits.length() <= 60; - - int clockSequence; - synchronized (CLOCK_SEQUENCE) { - clockSequence = CLOCK_SEQUENCE.incrementAndGet(); - if (clockSequence > 16384) - CLOCK_SEQUENCE.set(0); - } - BitSet clockSequenceBits = BitSet.valueOf(new long[] { clockSequence }); - - // Build the UUID, bit by bit - // see https://tools.ietf.org/html/rfc4122#section-4.2.2 - // time - BitSet time_low = new BitSet(32); - BitSet time_mid = new BitSet(16); - BitSet time_hi_and_version = new BitSet(16); - - for (int i = 0; i < 60; i++) { - if (i < 32) - time_low.set(i, timeBits.get(i)); - else if (i < 48) - time_mid.set(i - 32, timeBits.get(i)); - else - time_hi_and_version.set(i - 48, timeBits.get(i)); - } - // version - time_hi_and_version.set(12, true); - time_hi_and_version.set(13, false); - time_hi_and_version.set(14, false); - time_hi_and_version.set(15, false); - - // clock sequence - BitSet clk_seq_hi_res = new BitSet(8); - BitSet clk_seq_low = new BitSet(8); - for (int i = 0; i < 8; i++) { - clk_seq_low.set(i, clockSequenceBits.get(i)); - } - for (int i = 8; i < 14; i++) { - clk_seq_hi_res.set(i - 8, clockSequenceBits.get(i)); - } - // variant - clk_seq_hi_res.set(6, false); - clk_seq_hi_res.set(7, true); - -// String str = toHexString(time_low.toLongArray()[0]) + "-" + toHexString(time_mid.toLongArray()[0]) + "-" -// + toHexString(time_hi_and_version.toLongArray()[0]) + "-" -// + toHexString(clock_seq_hi_and_reserved.toLongArray()[0]) + toHexString(clock_seq_low.toLongArray()[0]) -// + "-" + toHexString(node.toLongArray()[0]); -// UUID uuid = UUID.fromString(str); - - BitSet uuidBits = new BitSet(128); - for (int i = 0; i < 128; i++) { - if (i < 48) - uuidBits.set(i, node.get(i)); - else if (i < 56) - uuidBits.set(i, clk_seq_low.get(i - 48)); - else if (i < 64) - uuidBits.set(i, clk_seq_hi_res.get(i - 56)); - else if (i < 80) - uuidBits.set(i, time_hi_and_version.get(i - 64)); - else if (i < 96) - uuidBits.set(i, time_mid.get(i - 80)); - else - uuidBits.set(i, time_low.get(i - 96)); - } - - long[] uuidLongs = uuidBits.toLongArray(); - assert uuidLongs.length == 2; - UUID uuid = new UUID(uuidLongs[1], uuidLongs[0]); - - // tests - assert uuid.node() == node.toLongArray()[0]; - assert uuid.timestamp() == timeNanos; - assert uuid.clockSequence() == clockSequence; - assert uuid.version() == 1; - assert uuid.variant() == 2; - return uuid; - } - - public static String toBinaryString(UUID uuid, int charsPerSegment, char separator) { - String binaryString = toBinaryString(uuid); - StringBuilder sb = new StringBuilder(128 + (128 / charsPerSegment)); - for (int i = 0; i < binaryString.length(); i++) { - if (i != 0 && i % charsPerSegment == 0) - sb.append(separator); - sb.append(binaryString.charAt(i)); - } - return sb.toString(); - } - - public static String toBinaryString(UUID uuid) { - String most = zeroTo64Chars(Long.toBinaryString(uuid.getMostSignificantBits())); - String least = zeroTo64Chars(Long.toBinaryString(uuid.getLeastSignificantBits())); - String binaryString = most + least; - assert binaryString.length() == 128; - return binaryString; - } - - private static String zeroTo64Chars(String str) { - assert str.length() <= 64; - if (str.length() < 64) { - StringBuilder sb = new StringBuilder(64); - for (int i = 0; i < 64 - str.length(); i++) - sb.append('0'); - sb.append(str); - return sb.toString(); - } else - return str; - } - - public static String compactToStd(String compact) { - if (compact.length() != 32) - throw new IllegalArgumentException( - "Compact UUID '" + compact + "' has length " + compact.length() + " and not 32."); - StringBuilder sb = new StringBuilder(36); - for (int i = 0; i < 32; i++) { - if (i == 8 || i == 12 || i == 16 || i == 20) - sb.append('-'); - sb.append(compact.charAt(i)); - } - String std = sb.toString(); - assert std.length() == 36; - assert UUID.fromString(std).toString().equals(std); - return std; - } - - public static UUID compactToUuid(String compact) { - return UUID.fromString(compactToStd(compact)); - } - - public static String firstBlock(UUID uuid) { - return uuid.toString().substring(0, 8); - } - - public static boolean isRandom(UUID uuid) { - return uuid.version() == 4; - } - - public static boolean isTimeBased(UUID uuid) { - return uuid.version() == 1; - } - - public static boolean isTimeBasedRandom(UUID uuid) { - if (uuid.version() == 1) { - BitSet node = BitSet.valueOf(new long[] { uuid.node() }); - return node.get(0); - } else - return false; - } - - public static boolean isNameBased(UUID uuid) { - return uuid.version() == 3 || uuid.version() == 5; - } - - /** Singleton. */ - private UuidUtils() { - } - - public final static void main(String[] args) throws Exception { - UUID uuid; - -// uuid = compactToUuid("996b1f5122de4b2f94e49168d32f22d1"); -// System.out.println(uuid.toString() + ", isRandom=" + isRandom(uuid)); - - // warm up before measuring perf - for (int i = 0; i < 10; i++) { - UUID.randomUUID(); - timeUUID(); - timeUUIDwithRandomNode(); - timeBasedRandomUUID(); - timeBasedUUID(); - } - - long begin; - long duration; - - begin = System.nanoTime(); - uuid = UUID.randomUUID(); - duration = System.nanoTime() - begin; - System.out.println(uuid.toString() + " in " + duration + " ns, isRandom=" + isRandom(uuid)); - - begin = System.nanoTime(); - uuid = timeUUID(); - duration = System.nanoTime() - begin; - System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid)); - - begin = System.nanoTime(); - uuid = timeUUIDwithRandomNode(); - duration = System.nanoTime() - begin; - System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid)); - - begin = System.nanoTime(); - uuid = timeBasedUUID(); - duration = System.nanoTime() - begin; - System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid)); - - begin = System.nanoTime(); - uuid = timeBasedRandomUUID(); - duration = System.nanoTime() - begin; - System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid)); -// System.out.println(toBinaryString(uuid, 8, ' ')); -// System.out.println(toBinaryString(uuid, 16, '\n')); - } -} diff --git a/org.argeo.enterprise/src/org/argeo/util/package-info.java b/org.argeo.enterprise/src/org/argeo/util/package-info.java deleted file mode 100644 index 4354b0a14..000000000 --- a/org.argeo.enterprise/src/org/argeo/util/package-info.java +++ /dev/null @@ -1,2 +0,0 @@ -/** Generic Java utilities. */ -package org.argeo.util; \ No newline at end of file diff --git a/org.argeo.enterprise/src/org/argeo/util/register/Component.java b/org.argeo.enterprise/src/org/argeo/util/register/Component.java deleted file mode 100644 index 4a812f83b..000000000 --- a/org.argeo.enterprise/src/org/argeo/util/register/Component.java +++ /dev/null @@ -1,283 +0,0 @@ -package org.argeo.util.register; - -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.function.Consumer; - -/** - * A wrapper for an object, whose dependencies and life cycle can be managed. - */ -public class Component { - - private final I instance; - - private final Runnable init; - private final Runnable close; - - private final Map, PublishedType> types; - private final Set> dependencies; - - private CompletableFuture activationStarted = null; - private CompletableFuture activated = null; - - private CompletableFuture deactivationStarted = null; - private CompletableFuture deactivated = null; - - private Set> dependants = new HashSet<>(); - - Component(Consumer> register, I instance, Runnable init, Runnable close, - Set> dependencies, Set> classes) { - assert instance != null; - assert init != null; - assert close != null; - assert dependencies != null; - assert classes != null; - - this.instance = instance; - this.init = init; - this.close = close; - - // types - Map, PublishedType> types = new HashMap<>(classes.size()); - for (Class clss : classes) { -// if (!clss.isAssignableFrom(instance.getClass())) -// throw new IllegalArgumentException( -// "Type " + clss.getName() + " is not compatible with " + instance.getClass().getName()); - types.put(clss, new PublishedType<>(this, clss)); - } - this.types = Collections.unmodifiableMap(types); - - // dependencies - this.dependencies = Collections.unmodifiableSet(new HashSet<>(dependencies)); - for (Dependency dependency : this.dependencies) { - dependency.setDependantComponent(this); - } - - // deactivated by default - deactivated = CompletableFuture.completedFuture(null); - deactivationStarted = CompletableFuture.completedFuture(null); - - // TODO check whether context is active, so that we start right away - prepareNextActivation(); - - register.accept(this); - } - - private void prepareNextActivation() { - activationStarted = new CompletableFuture(); - activated = activationStarted // - .thenComposeAsync(this::dependenciesActivated) // - .thenRun(this.init) // - .thenRun(() -> prepareNextDeactivation()); - } - - private void prepareNextDeactivation() { - deactivationStarted = new CompletableFuture(); - deactivated = deactivationStarted // - .thenComposeAsync(this::dependantsDeactivated) // - .thenRun(this.close) // - .thenRun(() -> prepareNextActivation()); - } - - public CompletableFuture getActivated() { - return activated; - } - - public CompletableFuture getDeactivated() { - return deactivated; - } - - void startActivating() { - if (activated.isDone() || activationStarted.isDone()) - return; - activationStarted.complete(null); - } - - void startDeactivating() { - if (deactivated.isDone() || deactivationStarted.isDone()) - return; - deactivationStarted.complete(null); - } - - CompletableFuture dependenciesActivated(Void v) { - Set> constraints = new HashSet<>(this.dependencies.size()); - for (Dependency dependency : this.dependencies) { - CompletableFuture dependencyActivated = dependency.publisherActivated() // - .thenCompose(dependency::set); - constraints.add(dependencyActivated); - } - return CompletableFuture.allOf(constraints.toArray(new CompletableFuture[constraints.size()])); - } - - CompletableFuture dependantsDeactivated(Void v) { - Set> constraints = new HashSet<>(this.dependants.size()); - for (Dependency dependant : this.dependants) { - CompletableFuture dependantDeactivated = dependant.dependantDeactivated() // - .thenCompose(dependant::unset); - constraints.add(dependantDeactivated); - } - CompletableFuture dependantsDeactivated = CompletableFuture - .allOf(constraints.toArray(new CompletableFuture[constraints.size()])); - return dependantsDeactivated; - - } - - void addDependant(Dependency dependant) { - dependants.add(dependant); - } - - I getInstance() { - return instance; - } - - @SuppressWarnings("unchecked") - PublishedType getType(Class clss) { - if (!types.containsKey(clss)) - throw new IllegalArgumentException(clss.getName() + " is not a type published by this component"); - return (PublishedType) types.get(clss); - } - - boolean isPublishedType(Class clss) { - return types.containsKey(clss); - } - - public static class PublishedType { - private Component component; - private Class clss; - -// private CompletableFuture> publisherAvailable; - private CompletableFuture value; - - public PublishedType(Component component, Class clss) { - this.clss = clss; - this.component = component; - value = CompletableFuture.completedFuture((T) component.instance); -// value = publisherAvailable.thenApply((c) -> c.getInstance()); - } - - Component getPublisher() { - return component; - } - -// CompletableFuture> publisherAvailable() { -// return publisherAvailable; -// } - - Class getType() { - return clss; - } - } - - public static class Builder { - private final I instance; - - private Runnable init; - private Runnable close; - - private Set> dependencies = new HashSet<>(); - private Set> types = new HashSet<>(); - - public Builder(I instance) { - this.instance = instance; - } - - public Component build(Consumer> register) { - // default values - if (types.isEmpty()) { - types.add(getInstanceClass()); - } - - if (init == null) - init = () -> { - }; - if (close == null) - close = () -> { - }; - - // instantiation - Component component = new Component(register, instance, init, close, dependencies, types); - for (Dependency dependency : dependencies) { - dependency.type.getPublisher().addDependant(dependency); - } - return component; - } - - public Builder addType(Class clss) { - types.add(clss); - return this; - } - - public Builder addInit(Runnable init) { - if (this.init != null) - throw new IllegalArgumentException("init method is already set"); - this.init = init; - return this; - } - - public Builder addClose(Runnable close) { - if (this.close != null) - throw new IllegalArgumentException("close method is already set"); - this.close = close; - return this; - } - - public Builder addDependency(PublishedType type, Consumer set, Consumer unset) { - dependencies.add(new Dependency(type, set, unset)); - return this; - } - - public I get() { - return instance; - } - - @SuppressWarnings("unchecked") - private Class getInstanceClass() { - return (Class) instance.getClass(); - } - - } - - static class Dependency { - private PublishedType type; - private Consumer set; - private Consumer unset; - - // live - Component dependantComponent; - CompletableFuture setStage; - CompletableFuture unsetStage; - - public Dependency(PublishedType types, Consumer set, Consumer unset) { - super(); - this.type = types; - this.set = set; - this.unset = unset != null ? unset : (v) -> set.accept(null); - } - - // live - void setDependantComponent(Component component) { - this.dependantComponent = component; - } - - CompletableFuture publisherActivated() { - return type.getPublisher().activated.copy(); - } - - CompletableFuture dependantDeactivated() { - return dependantComponent.deactivated.copy(); - } - - CompletableFuture set(Void v) { - return type.value.thenAccept(set); - } - - CompletableFuture unset(Void v) { - return type.value.thenAccept(unset); - } - - } -} diff --git a/org.argeo.enterprise/src/org/argeo/util/register/Register.java b/org.argeo.enterprise/src/org/argeo/util/register/Register.java deleted file mode 100644 index 17062809e..000000000 --- a/org.argeo.enterprise/src/org/argeo/util/register/Register.java +++ /dev/null @@ -1,10 +0,0 @@ -package org.argeo.util.register; - -import java.util.Map; - -/** A dynamic register of objects. */ -public interface Register { - Singleton set(T obj, Class clss, Map attributes, Class... classes); - - void shutdown(); -} diff --git a/org.argeo.enterprise/src/org/argeo/util/register/Singleton.java b/org.argeo.enterprise/src/org/argeo/util/register/Singleton.java deleted file mode 100644 index 5d70e9aeb..000000000 --- a/org.argeo.enterprise/src/org/argeo/util/register/Singleton.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.argeo.util.register; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.Future; -import java.util.function.Consumer; - -public class Singleton { - private final Class clss; - private final CompletableFuture registrationStage; - private final List> unregistrationHooks = new ArrayList<>(); - - public Singleton(Class clss, CompletableFuture registrationStage) { - this.clss = clss; - this.registrationStage = registrationStage; - } - - CompletionStage getRegistrationStage() { - return registrationStage.minimalCompletionStage(); - } - - public void addUnregistrationHook(Consumer todo) { - unregistrationHooks.add(todo); - } - - public Future getValue() { - return registrationStage.copy(); - } - - public CompletableFuture prepareUnregistration(Void v) { - List> lst = new ArrayList<>(); - for (Consumer hook : unregistrationHooks) { - lst.add(registrationStage.thenAcceptAsync(hook)); - } - CompletableFuture prepareUnregistrationStage = CompletableFuture - .allOf(lst.toArray(new CompletableFuture[lst.size()])); - return prepareUnregistrationStage; - } -} diff --git a/org.argeo.enterprise/src/org/argeo/util/register/StaticRegister.java b/org.argeo.enterprise/src/org/argeo/util/register/StaticRegister.java deleted file mode 100644 index c186aff08..000000000 --- a/org.argeo.enterprise/src/org/argeo/util/register/StaticRegister.java +++ /dev/null @@ -1,97 +0,0 @@ -package org.argeo.util.register; - -import java.util.HashSet; -import java.util.IdentityHashMap; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Consumer; -import java.util.function.Predicate; - -/** A minimal component register. */ -public class StaticRegister { - private final static AtomicBoolean started = new AtomicBoolean(false); - private final static IdentityHashMap> components = new IdentityHashMap<>(); - - public static Consumer> asConsumer() { - return (c) -> registerComponent(c); - } - -// public static BiFunction, Predicate>, Component> asProvider() { -// -// } - - static synchronized Component find(Class clss, Predicate> filter) { - Set> result = new HashSet<>(); - instances: for (Object instance : components.keySet()) { - if (!clss.isAssignableFrom(instance.getClass())) - continue instances; - Component component = (Component) components.get(instance); - - // TODO filter - if (component.isPublishedType(clss)) - result.add(component); - } - if (result.isEmpty()) - return null; - return result.iterator().next(); - - } - - static synchronized void registerComponent(Component component) { - if (started.get()) // TODO make it really dynamic - throw new IllegalStateException("Already activated"); - if (components.containsKey(component.getInstance())) - throw new IllegalArgumentException("Already registered as component"); - components.put(component.getInstance(), component); - } - - static synchronized Component get(Object instance) { - if (!components.containsKey(instance)) - throw new IllegalArgumentException("Not registered as component"); - return components.get(instance); - } - - synchronized static void activate() { - if (started.get()) - throw new IllegalStateException("Already activated"); - Set> constraints = new HashSet<>(); - for (Component component : components.values()) { - component.startActivating(); - constraints.add(component.getActivated()); - } - - // wait - try { - CompletableFuture.allOf(constraints.toArray(new CompletableFuture[0])).thenRun(() -> started.set(true)) - .get(); - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - synchronized static void deactivate() { - if (!started.get()) - throw new IllegalStateException("Not activated"); - Set> constraints = new HashSet<>(); - for (Component component : components.values()) { - component.startDeactivating(); - constraints.add(component.getDeactivated()); - } - - // wait - try { - CompletableFuture.allOf(constraints.toArray(new CompletableFuture[0])).thenRun(() -> started.set(false)) - .get(); - } catch (ExecutionException | InterruptedException e) { - throw new RuntimeException(e); - } - } - - synchronized static void clear() { - components.clear(); - } - -} diff --git a/org.argeo.util/.classpath b/org.argeo.util/.classpath new file mode 100644 index 000000000..71eb16789 --- /dev/null +++ b/org.argeo.util/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/org.argeo.util/.project b/org.argeo.util/.project new file mode 100644 index 000000000..171ff88dc --- /dev/null +++ b/org.argeo.util/.project @@ -0,0 +1,28 @@ + + + org.argeo.util + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.pde.PluginNature + + diff --git a/org.argeo.util/META-INF/.gitignore b/org.argeo.util/META-INF/.gitignore new file mode 100644 index 000000000..4854a41b9 --- /dev/null +++ b/org.argeo.util/META-INF/.gitignore @@ -0,0 +1 @@ +/MANIFEST.MF diff --git a/org.argeo.util/bnd.bnd b/org.argeo.util/bnd.bnd new file mode 100644 index 000000000..5f42f7786 --- /dev/null +++ b/org.argeo.util/bnd.bnd @@ -0,0 +1,6 @@ +Bundle-Activator: org.argeo.osgi.internal.EnterpriseActivator +Bundle-ActivationPolicy: lazy + +Import-Package: org.osgi.*;version=0.0.0,\ +!org.apache.commons.logging,\ +* diff --git a/org.argeo.util/build.properties b/org.argeo.util/build.properties new file mode 100644 index 000000000..ae2abc5ff --- /dev/null +++ b/org.argeo.util/build.properties @@ -0,0 +1 @@ +source.. = src/ \ No newline at end of file diff --git a/org.argeo.util/pom.xml b/org.argeo.util/pom.xml new file mode 100644 index 000000000..1494e321c --- /dev/null +++ b/org.argeo.util/pom.xml @@ -0,0 +1,12 @@ + + + 4.0.0 + + org.argeo.commons + argeo-commons + 2.3-SNAPSHOT + .. + + org.argeo.util + Java and OSGi utilities + \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/ident/IdentClient.java b/org.argeo.util/src/org/argeo/ident/IdentClient.java new file mode 100644 index 000000000..c42da9778 --- /dev/null +++ b/org.argeo.util/src/org/argeo/ident/IdentClient.java @@ -0,0 +1,124 @@ +package org.argeo.ident; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.ConnectException; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; +import java.util.StringTokenizer; + +/** + * A simple ident client, supporting authd OpenSSL encrypted username. + * + * @see RFC 1413 https://tools.ietf.org/html/rfc1413 + */ +public class IdentClient { + public final static int DEFAULT_IDENT_PORT = 113; + public final static String AUTHD_PASSPHRASE_PATH = "/etc/ident.key"; + final static String NO_USER = "NO-USER"; + + private final String host; + private final int port; + + private OpenSslDecryptor openSslDecryptor = new OpenSslDecryptor(); + private String identPassphrase = null; + + public IdentClient(String host) { + this(host, readPassphrase(AUTHD_PASSPHRASE_PATH), DEFAULT_IDENT_PORT); + } + + public IdentClient(String host, Path passPhrasePath) { + this(host, readPassphrase(passPhrasePath), DEFAULT_IDENT_PORT); + } + + public IdentClient(String host, String identPassphrase) { + this(host, identPassphrase, DEFAULT_IDENT_PORT); + } + + public IdentClient(String host, String identPassphrase, int port) { + this.host = host; + this.identPassphrase = identPassphrase; + this.port = port; + } + + /** @return the username or null if none */ + public String getUsername(int serverPort, int clientPort) { + String answer; + try (Socket socket = new Socket(host, port)) { + String msg = clientPort + "," + serverPort + "\n"; + OutputStream out = socket.getOutputStream(); + out.write(msg.getBytes(StandardCharsets.US_ASCII)); + out.flush(); + BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + answer = reader.readLine(); + } catch (ConnectException e) { + System.err.println( + "Ident client is configured but no ident server available on " + host + " (port " + port + ")"); + return null; + } catch (Exception e) { + throw new RuntimeException("Cannot read from ident server on " + host + " (port " + port + ")", e); + } + StringTokenizer st = new StringTokenizer(answer, " :\n"); + String username = null; + while (st.hasMoreTokens()) + username = st.nextToken(); + + if (username.equals(NO_USER)) + return null; + + if (identPassphrase != null && username.startsWith("[")) { + String encrypted = username.substring(1, username.length() - 1); + username = openSslDecryptor.decryptAuthd(encrypted, identPassphrase).trim(); + } +// System.out.println(username); + return username; + } + + public void setOpenSslDecryptor(OpenSslDecryptor openSslDecryptor) { + this.openSslDecryptor = openSslDecryptor; + } + + public static String readPassphrase(String filePath) { + return readPassphrase(Paths.get(filePath)); + } + + /** @return the first line of the file. */ + public static String readPassphrase(Path path) { + if (!isPathAvailable(path)) + return null; + List lines; + try { + lines = Files.readAllLines(path); + } catch (IOException e) { + throw new IllegalStateException("Cannot read " + path, e); + } + if (lines.size() == 0) + return null; + String passphrase = lines.get(0); + return passphrase; + } + + public static boolean isDefaultAuthdPassphraseFileAvailable() { + return isPathAvailable(Paths.get(AUTHD_PASSPHRASE_PATH)); + } + + public static boolean isPathAvailable(Path path) { + if (!Files.exists(path)) + return false; + if (!Files.isReadable(path)) + return false; + return true; + } + + public static void main(String[] args) { + IdentClient identClient = new IdentClient("127.0.0.1", "changeit"); + String username = identClient.getUsername(7070, 55958); + System.out.println(username); + } +} diff --git a/org.argeo.util/src/org/argeo/ident/OpenSslDecryptor.java b/org.argeo.util/src/org/argeo/ident/OpenSslDecryptor.java new file mode 100644 index 000000000..702b09bfe --- /dev/null +++ b/org.argeo.util/src/org/argeo/ident/OpenSslDecryptor.java @@ -0,0 +1,162 @@ +package org.argeo.ident; + +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.Base64; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * Decrypts OpenSSL encrypted data. + * + * From + * https://stackoverflow.com/questions/11783062/how-to-decrypt-file-in-java-encrypted-with-openssl-command-using-aes + * + * See also + * https://stackoverflow.com/questions/54171959/badpadding-exception-when-trying-to-decrypt-aes-based-encrypted-text/54173509#54173509 + * for new default message digest (not yet in CentOS 7 as of July 2019) + */ +public class OpenSslDecryptor { + private static final int INDEX_KEY = 0; + private static final int INDEX_IV = 1; + private static final int ITERATIONS = 1; + + private static final int SALT_OFFSET = 8; + private static final int SALT_SIZE = 8; + private static final int CIPHERTEXT_OFFSET = SALT_OFFSET + SALT_SIZE; + + /** In bits. */ + private final int keySize; + + private Cipher cipher; + private MessageDigest messageDigest; + + public OpenSslDecryptor() { + /* + * Changed to SHA-256 from OpenSSL v1.1.0 (see + * https://stackoverflow.com/questions/39637388/encryption-decryption-doesnt- + * work-well-between-two-different-openssl-versions) + */ + this(128, "MD5"); + } + + public OpenSslDecryptor(int keySize, String messageDigest) { + this.keySize = keySize; + try { + this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + this.messageDigest = MessageDigest.getInstance(messageDigest); + } catch (GeneralSecurityException e) { + throw new IllegalArgumentException("Cannot initialise decryptor", e); + } + } + + public String decryptAuthd(String dataBase64, String passphrase) { + try { + byte[] headerSaltAndCipherText = Base64.getDecoder().decode(dataBase64); + + boolean withSalt = true; + byte[] salt = withSalt ? Arrays.copyOfRange(headerSaltAndCipherText, SALT_OFFSET, SALT_OFFSET + SALT_SIZE) + : null; + byte[] encrypted = withSalt + ? Arrays.copyOfRange(headerSaltAndCipherText, CIPHERTEXT_OFFSET, headerSaltAndCipherText.length) + : headerSaltAndCipherText; + + final byte[][] keyAndIV = EVP_BytesToKey(keySize / Byte.SIZE, cipher.getBlockSize(), messageDigest, salt, + passphrase.getBytes(StandardCharsets.US_ASCII), ITERATIONS); + SecretKeySpec key = new SecretKeySpec(keyAndIV[INDEX_KEY], "AES"); + IvParameterSpec iv = new IvParameterSpec(keyAndIV[INDEX_IV]); + + cipher.init(Cipher.DECRYPT_MODE, key, iv); + byte[] decrypted = cipher.doFinal(encrypted); + + String answer = new String(decrypted, StandardCharsets.US_ASCII); + return answer; + } catch (BadPaddingException e) { + throw new IllegalStateException("Bad password, algorithm, mode or padding;" + + " no salt, wrong number of iterations or corrupted ciphertext.", e); + } catch (IllegalBlockSizeException e) { + throw new IllegalStateException("Bad algorithm, mode or corrupted (resized) ciphertext.", e); + } catch (GeneralSecurityException e) { + throw new IllegalStateException(e); + } + } + + private static byte[][] EVP_BytesToKey(int key_len, int iv_len, MessageDigest md, byte[] salt, byte[] data, + int count) { + byte[][] both = new byte[2][]; + byte[] key = new byte[key_len]; + int key_ix = 0; + byte[] iv = new byte[iv_len]; + int iv_ix = 0; + both[0] = key; + both[1] = iv; + byte[] md_buf = null; + int nkey = key_len; + int niv = iv_len; + int i = 0; + if (data == null) { + return both; + } + int addmd = 0; + for (;;) { + md.reset(); + if (addmd++ > 0) { + md.update(md_buf); + } + md.update(data); + if (null != salt) { + md.update(salt, 0, 8); + } + md_buf = md.digest(); + for (i = 1; i < count; i++) { + md.reset(); + md.update(md_buf); + md_buf = md.digest(); + } + i = 0; + if (nkey > 0) { + for (;;) { + if (nkey == 0) + break; + if (i == md_buf.length) + break; + key[key_ix++] = md_buf[i]; + nkey--; + i++; + } + } + if (niv > 0 && i != md_buf.length) { + for (;;) { + if (niv == 0) + break; + if (i == md_buf.length) + break; + iv[iv_ix++] = md_buf[i]; + niv--; + i++; + } + } + if (nkey == 0 && niv == 0) { + break; + } + } + for (i = 0; i < md_buf.length; i++) { + md_buf[i] = 0; + } + return both; + } + + public static void main(String[] args) { + String dataBase64 = args[0]; + String passphrase = args[1]; + OpenSslDecryptor decryptor = new OpenSslDecryptor(); + System.out.println(decryptor.decryptAuthd(dataBase64, passphrase)); + } + +} \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/ident/package-info.java b/org.argeo.util/src/org/argeo/ident/package-info.java new file mode 100644 index 000000000..35dd1a29e --- /dev/null +++ b/org.argeo.util/src/org/argeo/ident/package-info.java @@ -0,0 +1,2 @@ +/** Ident authentication protocol support. */ +package org.argeo.ident; \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/naming/AttributesDictionary.java b/org.argeo.util/src/org/argeo/naming/AttributesDictionary.java new file mode 100644 index 000000000..e04721610 --- /dev/null +++ b/org.argeo.util/src/org/argeo/naming/AttributesDictionary.java @@ -0,0 +1,171 @@ +package org.argeo.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/naming/AuthPassword.java b/org.argeo.util/src/org/argeo/naming/AuthPassword.java new file mode 100644 index 000000000..6d4c62b3f --- /dev/null +++ b/org.argeo.util/src/org/argeo/naming/AuthPassword.java @@ -0,0 +1,140 @@ +package org.argeo.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; + +import org.argeo.osgi.useradmin.UserDirectoryException; + +/** 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 UserDirectoryException("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/naming/Distinguished.java b/org.argeo.util/src/org/argeo/naming/Distinguished.java new file mode 100644 index 000000000..8b9c4b92b --- /dev/null +++ b/org.argeo.util/src/org/argeo/naming/Distinguished.java @@ -0,0 +1,36 @@ +package org.argeo.naming; + +import java.util.EnumSet; +import java.util.Set; +import java.util.TreeSet; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; + +/** + * An object that can be identified with an X.500 distinguished name. + * + * @see https://tools.ietf.org/html/rfc1779 + */ +public interface Distinguished { + /** The related distinguished name. */ + String dn(); + + /** The related distinguished name as an {@link LdapName}. */ + default LdapName distinguishedName() { + try { + return new LdapName(dn()); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Distinguished name " + dn() + " is not properly formatted.", e); + } + } + + /** List all DNs of an enumeration as strings. */ + static Set enumToDns(EnumSet enumSet) { + Set res = new TreeSet<>(); + for (Enum enm : enumSet) { + res.add(((Distinguished) enm).dn()); + } + return res; + } +} diff --git a/org.argeo.util/src/org/argeo/naming/DnsBrowser.java b/org.argeo.util/src/org/argeo/naming/DnsBrowser.java new file mode 100644 index 000000000..d9358c083 --- /dev/null +++ b/org.argeo.util/src/org/argeo/naming/DnsBrowser.java @@ -0,0 +1,179 @@ +package org.argeo.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/naming/LdapAttrs.csv b/org.argeo.util/src/org/argeo/naming/LdapAttrs.csv new file mode 100644 index 000000000..676d72720 --- /dev/null +++ b/org.argeo.util/src/org/argeo/naming/LdapAttrs.csv @@ -0,0 +1,129 @@ +uid,,,0.9.2342.19200300.100.1.1,,RFC 4519 +mail,,,0.9.2342.19200300.100.1.3,,RFC 4524 +info,,,0.9.2342.19200300.100.1.4,,RFC 4524 +drink,,,0.9.2342.19200300.100.1.5,,RFC 4524 +roomNumber,,,0.9.2342.19200300.100.1.6,,RFC 4524 +photo,,,0.9.2342.19200300.100.1.7,,RFC 2798 +userClass,,,0.9.2342.19200300.100.1.8,,RFC 4524 +host,,,0.9.2342.19200300.100.1.9,,RFC 4524 +manager,,,0.9.2342.19200300.100.1.10,,RFC 4524 +documentIdentifier,,,0.9.2342.19200300.100.1.11,,RFC 4524 +documentTitle,,,0.9.2342.19200300.100.1.12,,RFC 4524 +documentVersion,,,0.9.2342.19200300.100.1.13,,RFC 4524 +documentAuthor,,,0.9.2342.19200300.100.1.14,,RFC 4524 +documentLocation,,,0.9.2342.19200300.100.1.15,,RFC 4524 +homePhone,,,0.9.2342.19200300.100.1.20,,RFC 4524 +secretary,,,0.9.2342.19200300.100.1.21,,RFC 4524 +dc,,,0.9.2342.19200300.100.1.25,,RFC 4519 +associatedDomain,,,0.9.2342.19200300.100.1.37,,RFC 4524 +associatedName,,,0.9.2342.19200300.100.1.38,,RFC 4524 +homePostalAddress,,,0.9.2342.19200300.100.1.39,,RFC 4524 +personalTitle,,,0.9.2342.19200300.100.1.40,,RFC 4524 +mobile,,,0.9.2342.19200300.100.1.41,,RFC 4524 +pager,,,0.9.2342.19200300.100.1.42,,RFC 4524 +co,,,0.9.2342.19200300.100.1.43,,RFC 4524 +uniqueIdentifier,,,0.9.2342.19200300.100.1.44,,RFC 4524 +organizationalStatus,,,0.9.2342.19200300.100.1.45,,RFC 4524 +buildingName,,,0.9.2342.19200300.100.1.48,,RFC 4524 +audio,,,0.9.2342.19200300.100.1.55,,RFC 2798 +documentPublisher,,,0.9.2342.19200300.100.1.56,,RFC 4524 +jpegPhoto,,,0.9.2342.19200300.100.1.60,,RFC 2798 +vendorName,,,1.3.6.1.1.4,,RFC 3045 +vendorVersion,,,1.3.6.1.1.5,,RFC 3045 +entryUUID,,,1.3.6.1.1.16.4,,RFC 4530 +entryDN,,,1.3.6.1.1.20,,RFC 5020 +labeledURI,,,1.3.6.1.4.1.250.1.57,,RFC 2798 +numSubordinates,,,1.3.6.1.4.1.453.16.2.103,,draft-ietf-boreham-numsubordinates +namingContexts,,,1.3.6.1.4.1.1466.101.120.5,,RFC 4512 +altServer,,,1.3.6.1.4.1.1466.101.120.6,,RFC 4512 +supportedExtension,,,1.3.6.1.4.1.1466.101.120.7,,RFC 4512 +supportedControl,,,1.3.6.1.4.1.1466.101.120.13,,RFC 4512 +supportedSASLMechanisms,,,1.3.6.1.4.1.1466.101.120.14,,RFC 4512 +supportedLDAPVersion,,,1.3.6.1.4.1.1466.101.120.15,,RFC 4512 +ldapSyntaxes,,,1.3.6.1.4.1.1466.101.120.16,,RFC 4512 +supportedAuthPasswordSchemes,,,1.3.6.1.4.1.4203.1.3.3,,RFC 3112 +authPassword,,,1.3.6.1.4.1.4203.1.3.4,,RFC 3112 +supportedFeatures,,,1.3.6.1.4.1.4203.1.3.5,,RFC 4512 +inheritable,,,1.3.6.1.4.1.7628.5.4.1,,draft-ietf-ldup-subentry +blockInheritance,,,1.3.6.1.4.1.7628.5.4.2,,draft-ietf-ldup-subentry +objectClass,,,2.5.4.0,,RFC 4512 +aliasedObjectName,,,2.5.4.1,,RFC 4512 +cn,,,2.5.4.3,,RFC 4519 +sn,,,2.5.4.4,,RFC 4519 +serialNumber,,,2.5.4.5,,RFC 4519 +c,,,2.5.4.6,,RFC 4519 +l,,,2.5.4.7,,RFC 4519 +st,,,2.5.4.8,,RFC 4519 +street,,,2.5.4.9,,RFC 4519 +o,,,2.5.4.10,,RFC 4519 +ou,,,2.5.4.11,,RFC 4519 +title,,,2.5.4.12,,RFC 4519 +description,,,2.5.4.13,,RFC 4519 +searchGuide,,,2.5.4.14,,RFC 4519 +businessCategory,,,2.5.4.15,,RFC 4519 +postalAddress,,,2.5.4.16,,RFC 4519 +postalCode,,,2.5.4.17,,RFC 4519 +postOfficeBox,,,2.5.4.18,,RFC 4519 +physicalDeliveryOfficeName,,,2.5.4.19,,RFC 4519 +telephoneNumber,,,2.5.4.20,,RFC 4519 +telexNumber,,,2.5.4.21,,RFC 4519 +teletexTerminalIdentifier,,,2.5.4.22,,RFC 4519 +facsimileTelephoneNumber,,,2.5.4.23,,RFC 4519 +x121Address,,,2.5.4.24,,RFC 4519 +internationalISDNNumber,,,2.5.4.25,,RFC 4519 +registeredAddress,,,2.5.4.26,,RFC 4519 +destinationIndicator,,,2.5.4.27,,RFC 4519 +preferredDeliveryMethod,,,2.5.4.28,,RFC 4519 +member,,,2.5.4.31,,RFC 4519 +owner,,,2.5.4.32,,RFC 4519 +roleOccupant,,,2.5.4.33,,RFC 4519 +seeAlso,,,2.5.4.34,,RFC 4519 +userPassword,,,2.5.4.35,,RFC 4519 +userCertificate,,,2.5.4.36,,RFC 4523 +cACertificate,,,2.5.4.37,,RFC 4523 +authorityRevocationList,,,2.5.4.38,,RFC 4523 +certificateRevocationList,,,2.5.4.39,,RFC 4523 +crossCertificatePair,,,2.5.4.40,,RFC 4523 +name,,,2.5.4.41,,RFC 4519 +givenName,,,2.5.4.42,,RFC 4519 +initials,,,2.5.4.43,,RFC 4519 +generationQualifier,,,2.5.4.44,,RFC 4519 +x500UniqueIdentifier,,,2.5.4.45,,RFC 4519 +dnQualifier,,,2.5.4.46,,RFC 4519 +enhancedSearchGuide,,,2.5.4.47,,RFC 4519 +distinguishedName,,,2.5.4.49,,RFC 4519 +uniqueMember,,,2.5.4.50,,RFC 4519 +houseIdentifier,,,2.5.4.51,,RFC 4519 +supportedAlgorithms,,,2.5.4.52,,RFC 4523 +deltaRevocationList,,,2.5.4.53,,RFC 4523 +createTimestamp,,,2.5.18.1,,RFC 4512 +modifyTimestamp,,,2.5.18.2,,RFC 4512 +creatorsName,,,2.5.18.3,,RFC 4512 +modifiersName,,,2.5.18.4,,RFC 4512 +subschemaSubentry,,,2.5.18.10,,RFC 4512 +dITStructureRules,,,2.5.21.1,,RFC 4512 +dITContentRules,,,2.5.21.2,,RFC 4512 +matchingRules,,,2.5.21.4,,RFC 4512 +attributeTypes,,,2.5.21.5,,RFC 4512 +objectClasses,,,2.5.21.6,,RFC 4512 +nameForms,,,2.5.21.7,,RFC 4512 +matchingRuleUse,,,2.5.21.8,,RFC 4512 +structuralObjectClass,,,2.5.21.9,,RFC 4512 +governingStructureRule,,,2.5.21.10,,RFC 4512 +carLicense,,,2.16.840.1.113730.3.1.1,,RFC 2798 +departmentNumber,,,2.16.840.1.113730.3.1.2,,RFC 2798 +employeeNumber,,,2.16.840.1.113730.3.1.3,,RFC 2798 +employeeType,,,2.16.840.1.113730.3.1.4,,RFC 2798 +changeNumber,,,2.16.840.1.113730.3.1.5,,draft-good-ldap-changelog +targetDN,,,2.16.840.1.113730.3.1.6,,draft-good-ldap-changelog +changeType,,,2.16.840.1.113730.3.1.7,,draft-good-ldap-changelog +changes,,,2.16.840.1.113730.3.1.8,,draft-good-ldap-changelog +newRDN,,,2.16.840.1.113730.3.1.9,,draft-good-ldap-changelog +deleteOldRDN,,,2.16.840.1.113730.3.1.10,,draft-good-ldap-changelog +newSuperior,,,2.16.840.1.113730.3.1.11,,draft-good-ldap-changelog +ref,,,2.16.840.1.113730.3.1.34,,RFC 3296 +changelog,,,2.16.840.1.113730.3.1.35,,draft-good-ldap-changelog +preferredLanguage,,,2.16.840.1.113730.3.1.39,,RFC 2798 +userSMIMECertificate,,,2.16.840.1.113730.3.1.40,,RFC 2798 +userPKCS12,,,2.16.840.1.113730.3.1.216,,RFC 2798 +displayName,,,2.16.840.1.113730.3.1.241,,RFC 2798 diff --git a/org.argeo.util/src/org/argeo/naming/LdapAttrs.java b/org.argeo.util/src/org/argeo/naming/LdapAttrs.java new file mode 100644 index 000000000..cfabeb7d6 --- /dev/null +++ b/org.argeo.util/src/org/argeo/naming/LdapAttrs.java @@ -0,0 +1,341 @@ +package org.argeo.naming; + +/** + * Standard LDAP attributes as per:
+ * - Standard LDAP
+ * - Kerberos + * LDAP (partial) + */ +public enum LdapAttrs implements SpecifiedName { + /** */ + uid("0.9.2342.19200300.100.1.1", "RFC 4519"), + /** */ + mail("0.9.2342.19200300.100.1.3", "RFC 4524"), + /** */ + info("0.9.2342.19200300.100.1.4", "RFC 4524"), + /** */ + drink("0.9.2342.19200300.100.1.5", "RFC 4524"), + /** */ + roomNumber("0.9.2342.19200300.100.1.6", "RFC 4524"), + /** */ + photo("0.9.2342.19200300.100.1.7", "RFC 2798"), + /** */ + userClass("0.9.2342.19200300.100.1.8", "RFC 4524"), + /** */ + host("0.9.2342.19200300.100.1.9", "RFC 4524"), + /** */ + manager("0.9.2342.19200300.100.1.10", "RFC 4524"), + /** */ + documentIdentifier("0.9.2342.19200300.100.1.11", "RFC 4524"), + /** */ + documentTitle("0.9.2342.19200300.100.1.12", "RFC 4524"), + /** */ + documentVersion("0.9.2342.19200300.100.1.13", "RFC 4524"), + /** */ + documentAuthor("0.9.2342.19200300.100.1.14", "RFC 4524"), + /** */ + documentLocation("0.9.2342.19200300.100.1.15", "RFC 4524"), + /** */ + homePhone("0.9.2342.19200300.100.1.20", "RFC 4524"), + /** */ + secretary("0.9.2342.19200300.100.1.21", "RFC 4524"), + /** */ + dc("0.9.2342.19200300.100.1.25", "RFC 4519"), + /** */ + associatedDomain("0.9.2342.19200300.100.1.37", "RFC 4524"), + /** */ + associatedName("0.9.2342.19200300.100.1.38", "RFC 4524"), + /** */ + homePostalAddress("0.9.2342.19200300.100.1.39", "RFC 4524"), + /** */ + personalTitle("0.9.2342.19200300.100.1.40", "RFC 4524"), + /** */ + mobile("0.9.2342.19200300.100.1.41", "RFC 4524"), + /** */ + pager("0.9.2342.19200300.100.1.42", "RFC 4524"), + /** */ + co("0.9.2342.19200300.100.1.43", "RFC 4524"), + /** */ + uniqueIdentifier("0.9.2342.19200300.100.1.44", "RFC 4524"), + /** */ + organizationalStatus("0.9.2342.19200300.100.1.45", "RFC 4524"), + /** */ + buildingName("0.9.2342.19200300.100.1.48", "RFC 4524"), + /** */ + audio("0.9.2342.19200300.100.1.55", "RFC 2798"), + /** */ + documentPublisher("0.9.2342.19200300.100.1.56", "RFC 4524"), + /** */ + jpegPhoto("0.9.2342.19200300.100.1.60", "RFC 2798"), + /** */ + vendorName("1.3.6.1.1.4", "RFC 3045"), + /** */ + vendorVersion("1.3.6.1.1.5", "RFC 3045"), + /** */ + entryUUID("1.3.6.1.1.16.4", "RFC 4530"), + /** */ + entryDN("1.3.6.1.1.20", "RFC 5020"), + /** */ + labeledURI("1.3.6.1.4.1.250.1.57", "RFC 2798"), + /** */ + numSubordinates("1.3.6.1.4.1.453.16.2.103", "draft-ietf-boreham-numsubordinates"), + /** */ + namingContexts("1.3.6.1.4.1.1466.101.120.5", "RFC 4512"), + /** */ + altServer("1.3.6.1.4.1.1466.101.120.6", "RFC 4512"), + /** */ + supportedExtension("1.3.6.1.4.1.1466.101.120.7", "RFC 4512"), + /** */ + supportedControl("1.3.6.1.4.1.1466.101.120.13", "RFC 4512"), + /** */ + supportedSASLMechanisms("1.3.6.1.4.1.1466.101.120.14", "RFC 4512"), + /** */ + supportedLDAPVersion("1.3.6.1.4.1.1466.101.120.15", "RFC 4512"), + /** */ + ldapSyntaxes("1.3.6.1.4.1.1466.101.120.16", "RFC 4512"), + /** */ + supportedAuthPasswordSchemes("1.3.6.1.4.1.4203.1.3.3", "RFC 3112"), + /** */ + authPassword("1.3.6.1.4.1.4203.1.3.4", "RFC 3112"), + /** */ + supportedFeatures("1.3.6.1.4.1.4203.1.3.5", "RFC 4512"), + /** */ + inheritable("1.3.6.1.4.1.7628.5.4.1", "draft-ietf-ldup-subentry"), + /** */ + blockInheritance("1.3.6.1.4.1.7628.5.4.2", "draft-ietf-ldup-subentry"), + /** */ + objectClass("2.5.4.0", "RFC 4512"), + /** */ + aliasedObjectName("2.5.4.1", "RFC 4512"), + /** */ + cn("2.5.4.3", "RFC 4519"), + /** */ + sn("2.5.4.4", "RFC 4519"), + /** */ + serialNumber("2.5.4.5", "RFC 4519"), + /** */ + c("2.5.4.6", "RFC 4519"), + /** */ + l("2.5.4.7", "RFC 4519"), + /** */ + st("2.5.4.8", "RFC 4519"), + /** */ + street("2.5.4.9", "RFC 4519"), + /** */ + o("2.5.4.10", "RFC 4519"), + /** */ + ou("2.5.4.11", "RFC 4519"), + /** */ + title("2.5.4.12", "RFC 4519"), + /** */ + description("2.5.4.13", "RFC 4519"), + /** */ + searchGuide("2.5.4.14", "RFC 4519"), + /** */ + businessCategory("2.5.4.15", "RFC 4519"), + /** */ + postalAddress("2.5.4.16", "RFC 4519"), + /** */ + postalCode("2.5.4.17", "RFC 4519"), + /** */ + postOfficeBox("2.5.4.18", "RFC 4519"), + /** */ + physicalDeliveryOfficeName("2.5.4.19", "RFC 4519"), + /** */ + telephoneNumber("2.5.4.20", "RFC 4519"), + /** */ + telexNumber("2.5.4.21", "RFC 4519"), + /** */ + teletexTerminalIdentifier("2.5.4.22", "RFC 4519"), + /** */ + facsimileTelephoneNumber("2.5.4.23", "RFC 4519"), + /** */ + x121Address("2.5.4.24", "RFC 4519"), + /** */ + internationalISDNNumber("2.5.4.25", "RFC 4519"), + /** */ + registeredAddress("2.5.4.26", "RFC 4519"), + /** */ + destinationIndicator("2.5.4.27", "RFC 4519"), + /** */ + preferredDeliveryMethod("2.5.4.28", "RFC 4519"), + /** */ + member("2.5.4.31", "RFC 4519"), + /** */ + owner("2.5.4.32", "RFC 4519"), + /** */ + roleOccupant("2.5.4.33", "RFC 4519"), + /** */ + seeAlso("2.5.4.34", "RFC 4519"), + /** */ + userPassword("2.5.4.35", "RFC 4519"), + /** */ + userCertificate("2.5.4.36", "RFC 4523"), + /** */ + cACertificate("2.5.4.37", "RFC 4523"), + /** */ + authorityRevocationList("2.5.4.38", "RFC 4523"), + /** */ + certificateRevocationList("2.5.4.39", "RFC 4523"), + /** */ + crossCertificatePair("2.5.4.40", "RFC 4523"), + /** */ + name("2.5.4.41", "RFC 4519"), + /** */ + givenName("2.5.4.42", "RFC 4519"), + /** */ + initials("2.5.4.43", "RFC 4519"), + /** */ + generationQualifier("2.5.4.44", "RFC 4519"), + /** */ + x500UniqueIdentifier("2.5.4.45", "RFC 4519"), + /** */ + dnQualifier("2.5.4.46", "RFC 4519"), + /** */ + enhancedSearchGuide("2.5.4.47", "RFC 4519"), + /** */ + distinguishedName("2.5.4.49", "RFC 4519"), + /** */ + uniqueMember("2.5.4.50", "RFC 4519"), + /** */ + houseIdentifier("2.5.4.51", "RFC 4519"), + /** */ + supportedAlgorithms("2.5.4.52", "RFC 4523"), + /** */ + deltaRevocationList("2.5.4.53", "RFC 4523"), + /** */ + createTimestamp("2.5.18.1", "RFC 4512"), + /** */ + modifyTimestamp("2.5.18.2", "RFC 4512"), + /** */ + creatorsName("2.5.18.3", "RFC 4512"), + /** */ + modifiersName("2.5.18.4", "RFC 4512"), + /** */ + subschemaSubentry("2.5.18.10", "RFC 4512"), + /** */ + dITStructureRules("2.5.21.1", "RFC 4512"), + /** */ + dITContentRules("2.5.21.2", "RFC 4512"), + /** */ + matchingRules("2.5.21.4", "RFC 4512"), + /** */ + attributeTypes("2.5.21.5", "RFC 4512"), + /** */ + objectClasses("2.5.21.6", "RFC 4512"), + /** */ + nameForms("2.5.21.7", "RFC 4512"), + /** */ + matchingRuleUse("2.5.21.8", "RFC 4512"), + /** */ + structuralObjectClass("2.5.21.9", "RFC 4512"), + /** */ + governingStructureRule("2.5.21.10", "RFC 4512"), + /** */ + carLicense("2.16.840.1.113730.3.1.1", "RFC 2798"), + /** */ + departmentNumber("2.16.840.1.113730.3.1.2", "RFC 2798"), + /** */ + employeeNumber("2.16.840.1.113730.3.1.3", "RFC 2798"), + /** */ + employeeType("2.16.840.1.113730.3.1.4", "RFC 2798"), + /** */ + changeNumber("2.16.840.1.113730.3.1.5", "draft-good-ldap-changelog"), + /** */ + targetDN("2.16.840.1.113730.3.1.6", "draft-good-ldap-changelog"), + /** */ + changeType("2.16.840.1.113730.3.1.7", "draft-good-ldap-changelog"), + /** */ + changes("2.16.840.1.113730.3.1.8", "draft-good-ldap-changelog"), + /** */ + newRDN("2.16.840.1.113730.3.1.9", "draft-good-ldap-changelog"), + /** */ + deleteOldRDN("2.16.840.1.113730.3.1.10", "draft-good-ldap-changelog"), + /** */ + newSuperior("2.16.840.1.113730.3.1.11", "draft-good-ldap-changelog"), + /** */ + ref("2.16.840.1.113730.3.1.34", "RFC 3296"), + /** */ + changelog("2.16.840.1.113730.3.1.35", "draft-good-ldap-changelog"), + /** */ + preferredLanguage("2.16.840.1.113730.3.1.39", "RFC 2798"), + /** */ + userSMIMECertificate("2.16.840.1.113730.3.1.40", "RFC 2798"), + /** */ + userPKCS12("2.16.840.1.113730.3.1.216", "RFC 2798"), + /** */ + displayName("2.16.840.1.113730.3.1.241", "RFC 2798"), + + // Sun memberOf + memberOf("1.2.840.113556.1.2.102", "389 DS memberOf"), + + // KERBEROS (partial) + krbPrincipalName("2.16.840.1.113719.1.301.6.8.1", "Novell Kerberos Schema Definitions"), + + // RFC 2985 and RFC 3039 (partial) + dateOfBirth("1.3.6.1.5.5.7.9.1", "RFC 2985"), + /** */ + placeOfBirth("1.3.6.1.5.5.7.9.2", "RFC 2985"), + /** */ + gender("1.3.6.1.5.5.7.9.3", "RFC 2985"), + /** */ + countryOfCitizenship("1.3.6.1.5.5.7.9.4", "RFC 2985"), + /** */ + countryOfResidence("1.3.6.1.5.5.7.9.5", "RFC 2985"), + // + ; + + public final static String DN = "dn"; + +// private final static String LDAP_ = "ldap:"; + + private final String oid, spec; + + LdapAttrs(String oid, String spec) { + this.oid = oid; + this.spec = spec; + } + + @Override + public String getID() { + return oid; + } + + @Override + public String getSpec() { + return spec; + } + + public String getPrefix() { + return prefix(); + } + + public static String prefix() { + return "ldap"; + } + + public String property() { + return qualified(); + } + + public String qualified() { + String prefix = getPrefix(); + return prefix != null ? prefix + ":" + name() : name(); + } + + public String getNamespace() { + return namespace(); + } + + public static String namespace() { + return "http://www.argeo.org/ns/ldap"; + } + + @Override + public final String toString() { + // must return the name + return name(); + } + +} diff --git a/org.argeo.util/src/org/argeo/naming/LdapObjs.csv b/org.argeo.util/src/org/argeo/naming/LdapObjs.csv new file mode 100644 index 000000000..3d907cbeb --- /dev/null +++ b/org.argeo.util/src/org/argeo/naming/LdapObjs.csv @@ -0,0 +1,42 @@ +account,,,0.9.2342.19200300.100.4.5,,RFC 4524 +document,,,0.9.2342.19200300.100.4.6,,RFC 4524 +room,,,0.9.2342.19200300.100.4.7,,RFC 4524 +documentSeries,,,0.9.2342.19200300.100.4.9,,RFC 4524 +domain,,,0.9.2342.19200300.100.4.13,,RFC 4524 +rFC822localPart,,,0.9.2342.19200300.100.4.14,,RFC 4524 +domainRelatedObject,,,0.9.2342.19200300.100.4.17,,RFC 4524 +friendlyCountry,,,0.9.2342.19200300.100.4.18,,RFC 4524 +simpleSecurityObject,,,0.9.2342.19200300.100.4.19,,RFC 4524 +uidObject,,,1.3.6.1.1.3.1,,RFC 4519 +extensibleObject,,,1.3.6.1.4.1.1466.101.120.111,,RFC 4512 +dcObject,,,1.3.6.1.4.1.1466.344,,RFC 4519 +authPasswordObject,,,1.3.6.1.4.1.4203.1.4.7,,RFC 3112 +namedObject,,,1.3.6.1.4.1.5322.13.1.1,,draft-howard-namedobject +inheritableLDAPSubEntry,,,1.3.6.1.4.1.7628.5.6.1.1,,draft-ietf-ldup-subentry +top,,,2.5.6.0,,RFC 4512 +alias,,,2.5.6.1,,RFC 4512 +country,,,2.5.6.2,,RFC 4519 +locality,,,2.5.6.3,,RFC 4519 +organization,,,2.5.6.4,,RFC 4519 +organizationalUnit,,,2.5.6.5,,RFC 4519 +person,,,2.5.6.6,,RFC 4519 +organizationalPerson,,,2.5.6.7,,RFC 4519 +organizationalRole,,,2.5.6.8,,RFC 4519 +groupOfNames,,,2.5.6.9,,RFC 4519 +residentialPerson,,,2.5.6.10,,RFC 4519 +applicationProcess,,,2.5.6.11,,RFC 4519 +device,,,2.5.6.14,,RFC 4519 +strongAuthenticationUser,,,2.5.6.15,,RFC 4523 +certificationAuthority,,,2.5.6.16,,RFC 4523 +certificationAuthority-V2,,,2.5.6.16.2,,RFC 4523 +groupOfUniqueNames,,,2.5.6.17,,RFC 4519 +userSecurityInformation,,,2.5.6.18,,RFC 4523 +cRLDistributionPoint,,,2.5.6.19,,RFC 4523 +pkiUser,,,2.5.6.21,,RFC 4523 +pkiCA,,,2.5.6.22,,RFC 4523 +deltaCRL,,,2.5.6.23,,RFC 4523 +subschema,,,2.5.20.1,,RFC 4512 +ldapSubEntry,,,2.16.840.1.113719.2.142.6.1.1,,draft-ietf-ldup-subentry +changeLogEntry,,,2.16.840.1.113730.3.2.1,,draft-good-ldap-changelog +inetOrgPerson,,,2.16.840.1.113730.3.2.2,,RFC 2798 +referral,,,2.16.840.1.113730.3.2.6,,RFC 3296 diff --git a/org.argeo.util/src/org/argeo/naming/LdapObjs.java b/org.argeo.util/src/org/argeo/naming/LdapObjs.java new file mode 100644 index 000000000..061167513 --- /dev/null +++ b/org.argeo.util/src/org/argeo/naming/LdapObjs.java @@ -0,0 +1,114 @@ +package org.argeo.naming; + +/** + * Standard LDAP object classes as per + * https://www.ldap.com/ldap- + * oid-reference + */ +public enum LdapObjs implements SpecifiedName { + account("0.9.2342.19200300.100.4.5", "RFC 4524"), + /** */ + document("0.9.2342.19200300.100.4.6", "RFC 4524"), + /** */ + room("0.9.2342.19200300.100.4.7", "RFC 4524"), + /** */ + documentSeries("0.9.2342.19200300.100.4.9", "RFC 4524"), + /** */ + domain("0.9.2342.19200300.100.4.13", "RFC 4524"), + /** */ + rFC822localPart("0.9.2342.19200300.100.4.14", "RFC 4524"), + /** */ + domainRelatedObject("0.9.2342.19200300.100.4.17", "RFC 4524"), + /** */ + friendlyCountry("0.9.2342.19200300.100.4.18", "RFC 4524"), + /** */ + simpleSecurityObject("0.9.2342.19200300.100.4.19", "RFC 4524"), + /** */ + uidObject("1.3.6.1.1.3.1", "RFC 4519"), + /** */ + extensibleObject("1.3.6.1.4.1.1466.101.120.111", "RFC 4512"), + /** */ + dcObject("1.3.6.1.4.1.1466.344", "RFC 4519"), + /** */ + authPasswordObject("1.3.6.1.4.1.4203.1.4.7", "RFC 3112"), + /** */ + namedObject("1.3.6.1.4.1.5322.13.1.1", "draft-howard-namedobject"), + /** */ + inheritableLDAPSubEntry("1.3.6.1.4.1.7628.5.6.1.1", "draft-ietf-ldup-subentry"), + /** */ + top("2.5.6.0", "RFC 4512"), + /** */ + alias("2.5.6.1", "RFC 4512"), + /** */ + country("2.5.6.2", "RFC 4519"), + /** */ + locality("2.5.6.3", "RFC 4519"), + /** */ + organization("2.5.6.4", "RFC 4519"), + /** */ + organizationalUnit("2.5.6.5", "RFC 4519"), + /** */ + person("2.5.6.6", "RFC 4519"), + /** */ + organizationalPerson("2.5.6.7", "RFC 4519"), + /** */ + organizationalRole("2.5.6.8", "RFC 4519"), + /** */ + groupOfNames("2.5.6.9", "RFC 4519"), + /** */ + residentialPerson("2.5.6.10", "RFC 4519"), + /** */ + applicationProcess("2.5.6.11", "RFC 4519"), + /** */ + device("2.5.6.14", "RFC 4519"), + /** */ + strongAuthenticationUser("2.5.6.15", "RFC 4523"), + /** */ + certificationAuthority("2.5.6.16", "RFC 4523"), + // /** Should be certificationAuthority-V2 */ + // certificationAuthority_V2("2.5.6.16.2", "RFC 4523") { + // }, + /** */ + groupOfUniqueNames("2.5.6.17", "RFC 4519"), + /** */ + userSecurityInformation("2.5.6.18", "RFC 4523"), + /** */ + cRLDistributionPoint("2.5.6.19", "RFC 4523"), + /** */ + pkiUser("2.5.6.21", "RFC 4523"), + /** */ + pkiCA("2.5.6.22", "RFC 4523"), + /** */ + deltaCRL("2.5.6.23", "RFC 4523"), + /** */ + subschema("2.5.20.1", "RFC 4512"), + /** */ + ldapSubEntry("2.16.840.1.113719.2.142.6.1.1", "draft-ietf-ldup-subentry"), + /** */ + changeLogEntry("2.16.840.1.113730.3.2.1", "draft-good-ldap-changelog"), + /** */ + inetOrgPerson("2.16.840.1.113730.3.2.2", "RFC 2798"), + /** */ + referral("2.16.840.1.113730.3.2.6", "RFC 3296"); + + private final static String LDAP_ = "ldap:"; + private final String oid, spec; + + private LdapObjs(String oid, String spec) { + this.oid = oid; + this.spec = spec; + } + + public String getOid() { + return oid; + } + + public String getSpec() { + return spec; + } + + public String property() { + return new StringBuilder(LDAP_).append(name()).toString(); + } + +} diff --git a/org.argeo.util/src/org/argeo/naming/LdifParser.java b/org.argeo.util/src/org/argeo/naming/LdifParser.java new file mode 100644 index 000000000..cc1957058 --- /dev/null +++ b/org.argeo.util/src/org/argeo/naming/LdifParser.java @@ -0,0 +1,161 @@ +package org.argeo.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; + +import org.argeo.osgi.useradmin.UserDirectoryException; + +/** 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 UserDirectoryException( + "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 UserDirectoryException("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/naming/LdifWriter.java b/org.argeo.util/src/org/argeo/naming/LdifWriter.java new file mode 100644 index 000000000..98d2df055 --- /dev/null +++ b/org.argeo.util/src/org/argeo/naming/LdifWriter.java @@ -0,0 +1,106 @@ +package org.argeo.naming; + +import static org.argeo.naming.LdapAttrs.DN; +import static org.argeo.naming.LdapAttrs.member; +import static org.argeo.naming.LdapAttrs.objectClass; +import static org.argeo.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; + +import org.argeo.osgi.useradmin.UserDirectoryException; + +/** 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 UserDirectoryException( + "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 UserDirectoryException("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/naming/NamingUtils.java b/org.argeo.util/src/org/argeo/naming/NamingUtils.java new file mode 100644 index 000000000..5a868ddb4 --- /dev/null +++ b/org.argeo.util/src/org/argeo/naming/NamingUtils.java @@ -0,0 +1,106 @@ +package org.argeo.naming; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoField; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public class NamingUtils { + /** As per https://tools.ietf.org/html/rfc4517#section-3.3.13 */ + private final static DateTimeFormatter utcLdapDate = DateTimeFormatter.ofPattern("uuuuMMddHHmmssX") + .withZone(ZoneOffset.UTC); + + /** @return null if not parseable */ + public static Instant ldapDateToInstant(String ldapDate) { + try { + return OffsetDateTime.parse(ldapDate, utcLdapDate).toInstant(); + } catch (DateTimeParseException e) { + return null; + } + } + + /** @return null if not parseable */ + public static ZonedDateTime ldapDateToZonedDateTime(String ldapDate) { + try { + return OffsetDateTime.parse(ldapDate, utcLdapDate).toZonedDateTime(); + } catch (DateTimeParseException e) { + return null; + } + } + + public static Calendar ldapDateToCalendar(String ldapDate) { + OffsetDateTime instant = OffsetDateTime.parse(ldapDate, utcLdapDate); + GregorianCalendar calendar = new GregorianCalendar(); + calendar.set(Calendar.DAY_OF_MONTH, instant.get(ChronoField.DAY_OF_MONTH)); + calendar.set(Calendar.MONTH, instant.get(ChronoField.MONTH_OF_YEAR)); + calendar.set(Calendar.YEAR, instant.get(ChronoField.YEAR)); + return calendar; + } + + public static String instantToLdapDate(ZonedDateTime instant) { + return utcLdapDate.format(instant.withZoneSameInstant(ZoneOffset.UTC)); + } + + public static String getQueryValue(Map> query, String key) { + if (!query.containsKey(key)) + return null; + List val = query.get(key); + if (val.size() == 1) + return val.get(0); + else + throw new IllegalArgumentException("There are " + val.size() + " value(s) for " + key); + } + + public static Map> queryToMap(URI uri) { + return queryToMap(uri.getQuery()); + } + + private static Map> queryToMap(String queryPart) { + try { + final Map> query_pairs = new LinkedHashMap>(); + if (queryPart == null) + return query_pairs; + final String[] pairs = queryPart.split("&"); + for (String pair : pairs) { + final int idx = pair.indexOf("="); + final String key = idx > 0 ? URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8.name()) + : pair; + if (!query_pairs.containsKey(key)) { + query_pairs.put(key, new LinkedList()); + } + final String value = idx > 0 && pair.length() > idx + 1 + ? URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8.name()) + : null; + query_pairs.get(key).add(value); + } + return query_pairs; + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("Cannot convert " + queryPart + " to map", e); + } + } + + private NamingUtils() { + + } + + public static void main(String args[]) { + ZonedDateTime now = ZonedDateTime.now().withZoneSameInstant(ZoneOffset.UTC); + String str = utcLdapDate.format(now); + System.out.println(str); + utcLdapDate.parse(str); + utcLdapDate.parse("19520512000000Z"); + } +} diff --git a/org.argeo.util/src/org/argeo/naming/SharedSecret.java b/org.argeo.util/src/org/argeo/naming/SharedSecret.java new file mode 100644 index 000000000..369b411fc --- /dev/null +++ b/org.argeo.util/src/org/argeo/naming/SharedSecret.java @@ -0,0 +1,46 @@ +package org.argeo.naming; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + +public class SharedSecret extends AuthPassword { + public final static String X_SHARED_SECRET = "X-SharedSecret"; + private final Instant expiry; + + public SharedSecret(String authInfo, String authValue) { + super(authInfo, authValue); + expiry = null; + } + + public SharedSecret(AuthPassword authPassword) { + super(authPassword); + String authInfo = getAuthInfo(); + if (authInfo.length() == 16) { + expiry = NamingUtils.ldapDateToInstant(authInfo); + } else { + expiry = null; + } + } + + public SharedSecret(ZonedDateTime expiryTimestamp, String value) { + super(NamingUtils.instantToLdapDate(expiryTimestamp), value); + expiry = expiryTimestamp.withZoneSameInstant(ZoneOffset.UTC).toInstant(); + } + + public SharedSecret(int hours, String value) { + this(ZonedDateTime.now().plusHours(hours), value); + } + + @Override + protected String getExpectedAuthScheme() { + return X_SHARED_SECRET; + } + + public boolean isExpired() { + if (expiry == null) + return false; + return expiry.isBefore(Instant.now()); + } + +} diff --git a/org.argeo.util/src/org/argeo/naming/SpecifiedName.java b/org.argeo.util/src/org/argeo/naming/SpecifiedName.java new file mode 100644 index 000000000..28cc2f9da --- /dev/null +++ b/org.argeo.util/src/org/argeo/naming/SpecifiedName.java @@ -0,0 +1,20 @@ +package org.argeo.naming; + +/** + * A name which has been specified and for which an id has been defined + * (typically an OID). + */ +public interface SpecifiedName { + /** The name */ + String name(); + + /** An RFC or the URLof some specification */ + default String getSpec() { + return null; + } + + /** Typically an OID */ + default String getID() { + return getClass().getName() + "." + name(); + } +} diff --git a/org.argeo.util/src/org/argeo/naming/SrvRecord.java b/org.argeo.util/src/org/argeo/naming/SrvRecord.java new file mode 100644 index 000000000..8ecc94457 --- /dev/null +++ b/org.argeo.util/src/org/argeo/naming/SrvRecord.java @@ -0,0 +1,52 @@ +package org.argeo.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/naming/package-info.java b/org.argeo.util/src/org/argeo/naming/package-info.java new file mode 100644 index 000000000..95e7de313 --- /dev/null +++ b/org.argeo.util/src/org/argeo/naming/package-info.java @@ -0,0 +1,2 @@ +/** Generic naming and LDAP support. */ +package org.argeo.naming; \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/osgi/internal/EnterpriseActivator.java b/org.argeo.util/src/org/argeo/osgi/internal/EnterpriseActivator.java new file mode 100644 index 000000000..bb495dd12 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/internal/EnterpriseActivator.java @@ -0,0 +1,21 @@ +package org.argeo.osgi.internal; + +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; + +/** + * Called to gather information about the OSGi runtime. Should not activate + * anything else that canonical monitoring services (not creating implicit + * APIs), which is the responsibility of higher levels. + */ +public class EnterpriseActivator implements BundleActivator { + + @Override + public void start(BundleContext context) throws Exception { + } + + @Override + public void stop(BundleContext context) throws Exception { + } + +} diff --git a/org.argeo.util/src/org/argeo/osgi/metatype/EnumAD.java b/org.argeo.util/src/org/argeo/osgi/metatype/EnumAD.java new file mode 100644 index 000000000..44b429934 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/metatype/EnumAD.java @@ -0,0 +1,60 @@ +package org.argeo.osgi.metatype; + +import org.argeo.naming.SpecifiedName; +import org.osgi.service.metatype.AttributeDefinition; + +public interface EnumAD extends SpecifiedName, AttributeDefinition { + String name(); + + default Object getDefault() { + return null; + } + + @Override + default String getName() { + return name(); + } + + @Override + default String getID() { + return getClass().getName() + "." + name(); + } + + @Override + default String getDescription() { + return null; + } + + @Override + default int getCardinality() { + return 0; + } + + @Override + default int getType() { + return STRING; + } + + @Override + default String[] getOptionValues() { + return null; + } + + @Override + default String[] getOptionLabels() { + return null; + } + + @Override + default String validate(String value) { + return null; + } + + @Override + default String[] getDefaultValue() { + Object value = getDefault(); + if (value == null) + return null; + return new String[] { value.toString() }; + } +} diff --git a/org.argeo.util/src/org/argeo/osgi/metatype/EnumOCD.java b/org.argeo.util/src/org/argeo/osgi/metatype/EnumOCD.java new file mode 100644 index 000000000..97c7d56e1 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/metatype/EnumOCD.java @@ -0,0 +1,54 @@ +package org.argeo.osgi.metatype; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; + +import org.osgi.service.metatype.AttributeDefinition; +import org.osgi.service.metatype.ObjectClassDefinition; + +public class EnumOCD> implements ObjectClassDefinition { + private final Class enumClass; + private String locale; + + public EnumOCD(Class clazz, String locale) { + this.enumClass = clazz; + this.locale = locale; + } + + @Override + public String getName() { + return null; + } + + public String getLocale() { + return locale; + } + + @Override + public String getID() { + return enumClass.getName(); + } + + @Override + public String getDescription() { + return null; + } + + @Override + public AttributeDefinition[] getAttributeDefinitions(int filter) { + EnumSet set = EnumSet.allOf(enumClass); + List attrs = new ArrayList<>(); + for (T key : set) + attrs.add((AttributeDefinition) key); + return attrs.toArray(new AttributeDefinition[attrs.size()]); + } + + @Override + public InputStream getIcon(int size) throws IOException { + return null; + } + +} diff --git a/org.argeo.util/src/org/argeo/osgi/metatype/package-info.java b/org.argeo.util/src/org/argeo/osgi/metatype/package-info.java new file mode 100644 index 000000000..bca5d1fda --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/metatype/package-info.java @@ -0,0 +1,2 @@ +/** OSGi metatype support. */ +package org.argeo.osgi.metatype; \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/osgi/provisioning/SimpleProvisioningService.java b/org.argeo.util/src/org/argeo/osgi/provisioning/SimpleProvisioningService.java new file mode 100644 index 000000000..c0ec29000 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/provisioning/SimpleProvisioningService.java @@ -0,0 +1,118 @@ +package org.argeo.osgi.provisioning; + +import java.io.IOException; +import java.util.Collections; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.Map; +import java.util.TreeMap; +import java.util.zip.ZipInputStream; + +import org.osgi.service.provisioning.ProvisioningService; + +public class SimpleProvisioningService implements ProvisioningService { + private Map map = Collections.synchronizedSortedMap(new TreeMap<>()); + + public SimpleProvisioningService() { + // update count + map.put(PROVISIONING_UPDATE_COUNT, 0); + } + + @Override + public Dictionary getInformation() { + return new Information(); + } + + @Override + public synchronized void setInformation(Dictionary info) { + map.clear(); + addInformation(info); + } + + @Override + public synchronized void addInformation(Dictionary info) { + Enumeration e = info.keys(); + while (e.hasMoreElements()) { + String key = e.nextElement(); + map.put(key, info.get(key)); + } + incrementProvisioningUpdateCount(); + } + + protected synchronized void incrementProvisioningUpdateCount() { + Integer current = (Integer) map.get(PROVISIONING_UPDATE_COUNT); + Integer newValue = current + 1; + map.put(PROVISIONING_UPDATE_COUNT, newValue); + } + + @Override + public synchronized void addInformation(ZipInputStream zis) throws IOException { + throw new UnsupportedOperationException(); + } + + class Information extends Dictionary { + + @Override + public int size() { + return map.size(); + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public Enumeration keys() { + Iterator it = map.keySet().iterator(); + return new Enumeration() { + + @Override + public boolean hasMoreElements() { + return it.hasNext(); + } + + @Override + public String nextElement() { + return it.next(); + } + + }; + } + + @Override + public Enumeration elements() { + Iterator it = map.values().iterator(); + return new Enumeration() { + + @Override + public boolean hasMoreElements() { + return it.hasNext(); + } + + @Override + public Object nextElement() { + return it.next(); + } + + }; + } + + @Override + public Object get(Object key) { + return map.get(key); + } + + @Override + public Object put(String key, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public Object remove(Object key) { + throw new UnsupportedOperationException(); + } + + } +} diff --git a/org.argeo.util/src/org/argeo/osgi/provisioning/package-info.java b/org.argeo.util/src/org/argeo/osgi/provisioning/package-info.java new file mode 100644 index 000000000..1859887e2 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/provisioning/package-info.java @@ -0,0 +1,2 @@ +/** OSGi provisioning support. */ +package org.argeo.osgi.provisioning; \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/osgi/transaction/JtaStatusAdapter.java b/org.argeo.util/src/org/argeo/osgi/transaction/JtaStatusAdapter.java new file mode 100644 index 000000000..2dd94c654 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/transaction/JtaStatusAdapter.java @@ -0,0 +1,61 @@ +package org.argeo.osgi.transaction; + +/** JTA transaction status. */ +public class JtaStatusAdapter implements TransactionStatusAdapter { + private static final Integer STATUS_ACTIVE = 0; + private static final Integer STATUS_COMMITTED = 3; + private static final Integer STATUS_COMMITTING = 8; + private static final Integer STATUS_MARKED_ROLLBACK = 1; + private static final Integer STATUS_NO_TRANSACTION = 6; + private static final Integer STATUS_PREPARED = 2; + private static final Integer STATUS_PREPARING = 7; + private static final Integer STATUS_ROLLEDBACK = 4; + private static final Integer STATUS_ROLLING_BACK = 9; +// private static final Integer STATUS_UNKNOWN = 5; + + @Override + public Integer getActiveStatus() { + return STATUS_ACTIVE; + } + + @Override + public Integer getPreparingStatus() { + return STATUS_PREPARING; + } + + @Override + public Integer getMarkedRollbackStatus() { + return STATUS_MARKED_ROLLBACK; + } + + @Override + public Integer getPreparedStatus() { + return STATUS_PREPARED; + } + + @Override + public Integer getCommittingStatus() { + return STATUS_COMMITTING; + } + + @Override + public Integer getCommittedStatus() { + return STATUS_COMMITTED; + } + + @Override + public Integer getRollingBackStatus() { + return STATUS_ROLLING_BACK; + } + + @Override + public Integer getRolledBackStatus() { + return STATUS_ROLLEDBACK; + } + + @Override + public Integer getNoTransactionStatus() { + return STATUS_NO_TRANSACTION; + } + +} diff --git a/org.argeo.util/src/org/argeo/osgi/transaction/SimpleRollbackException.java b/org.argeo.util/src/org/argeo/osgi/transaction/SimpleRollbackException.java new file mode 100644 index 000000000..cf8a80b83 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/transaction/SimpleRollbackException.java @@ -0,0 +1,15 @@ +package org.argeo.osgi.transaction; + +/** Internal unchecked rollback exception. */ +class SimpleRollbackException extends RuntimeException { + private static final long serialVersionUID = 8055601819719780566L; + + public SimpleRollbackException() { + super(); + } + + public SimpleRollbackException(Throwable cause) { + super(cause); + } + +} diff --git a/org.argeo.util/src/org/argeo/osgi/transaction/SimpleTransaction.java b/org.argeo.util/src/org/argeo/osgi/transaction/SimpleTransaction.java new file mode 100644 index 000000000..e668b317e --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/transaction/SimpleTransaction.java @@ -0,0 +1,160 @@ +package org.argeo.osgi.transaction; + +import java.util.ArrayList; +import java.util.List; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +/** Simple implementation of an XA transaction. */ +class SimpleTransaction +//implements Transaction, Status +{ + private final Xid xid; + private T status; + private final List xaResources = new ArrayList(); + + private final SimpleTransactionManager transactionManager; + private TransactionStatusAdapter tsa; + + public SimpleTransaction(SimpleTransactionManager transactionManager, TransactionStatusAdapter tsa) { + this.tsa = tsa; + this.status = tsa.getActiveStatus(); + this.xid = new UuidXid(); + this.transactionManager = transactionManager; + } + + public synchronized void commit() +// throws RollbackException, HeuristicMixedException, HeuristicRollbackException, +// SecurityException, IllegalStateException, SystemException + { + status = tsa.getPreparingStatus(); + for (XAResource xaRes : xaResources) { + if (status.equals(tsa.getMarkedRollbackStatus())) + break; + try { + xaRes.prepare(xid); + } catch (XAException e) { + status = tsa.getMarkedRollbackStatus(); + error("Cannot prepare " + xaRes + " for " + xid, e); + } + } + if (status.equals(tsa.getMarkedRollbackStatus())) { + rollback(); + throw new SimpleRollbackException(); + } + status = tsa.getPreparedStatus(); + + status = tsa.getCommittingStatus(); + for (XAResource xaRes : xaResources) { + if (status.equals(tsa.getMarkedRollbackStatus())) + break; + try { + xaRes.commit(xid, false); + } catch (XAException e) { + status = tsa.getMarkedRollbackStatus(); + error("Cannot prepare " + xaRes + " for " + xid, e); + } + } + if (status.equals(tsa.getMarkedRollbackStatus())) { + rollback(); + throw new SimpleRollbackException(); + } + + // complete + status = tsa.getCommittedStatus(); + clearResources(XAResource.TMSUCCESS); + transactionManager.unregister(xid); + } + + public synchronized void rollback() +// throws IllegalStateException, SystemException + { + status = tsa.getRollingBackStatus(); + for (XAResource xaRes : xaResources) { + try { + xaRes.rollback(xid); + } catch (XAException e) { + error("Cannot rollback " + xaRes + " for " + xid, e); + } + } + + // complete + status = tsa.getRolledBackStatus(); + clearResources(XAResource.TMFAIL); + transactionManager.unregister(xid); + } + + public synchronized boolean enlistResource(XAResource xaRes) +// throws RollbackException, IllegalStateException, SystemException + { + if (xaResources.add(xaRes)) { + try { + xaRes.start(getXid(), XAResource.TMNOFLAGS); + return true; + } catch (XAException e) { + error("Cannot enlist " + xaRes, e); + return false; + } + } else + return false; + } + + public synchronized boolean delistResource(XAResource xaRes, int flag) +// throws IllegalStateException, SystemException + { + if (xaResources.remove(xaRes)) { + try { + xaRes.end(getXid(), flag); + } catch (XAException e) { + error("Cannot delist " + xaRes, e); + return false; + } + return true; + } else + return false; + } + + protected void clearResources(int flag) { + for (XAResource xaRes : xaResources) + try { + xaRes.end(getXid(), flag); + } catch (XAException e) { + error("Cannot end " + xaRes, e); + } + xaResources.clear(); + } + + protected void error(Object obj, Exception e) { + System.err.println(obj); + e.printStackTrace(); + } + + public synchronized T getStatus() +// throws SystemException + { + return status; + } + +// public void registerSynchronization(Synchronization sync) +// throws RollbackException, IllegalStateException, SystemException { +// throw new UnsupportedOperationException(); +// } + + public void setRollbackOnly() +// throws IllegalStateException, SystemException + { + status = tsa.getMarkedRollbackStatus(); + } + + @Override + public int hashCode() { + return xid.hashCode(); + } + + Xid getXid() { + return xid; + } + +} diff --git a/org.argeo.util/src/org/argeo/osgi/transaction/SimpleTransactionManager.java b/org.argeo.util/src/org/argeo/osgi/transaction/SimpleTransactionManager.java new file mode 100644 index 000000000..3d4edfd17 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/transaction/SimpleTransactionManager.java @@ -0,0 +1,214 @@ +package org.argeo.osgi.transaction; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.Callable; + +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +/** + * Simple implementation of an XA transaction manager. + */ +public class SimpleTransactionManager +// implements TransactionManager, UserTransaction + implements WorkControl, WorkTransaction { + private ThreadLocal> current = new ThreadLocal>(); + + private Map> knownTransactions = Collections + .synchronizedMap(new HashMap>()); + private TransactionStatusAdapter tsa = new JtaStatusAdapter(); +// private SyncRegistry syncRegistry = new SyncRegistry(); + + /* + * WORK IMPLEMENTATION + */ + @Override + public T required(Callable work) { + T res; + begin(); + try { + res = work.call(); + commit(); + } catch (Exception e) { + rollback(); + throw new SimpleRollbackException(e); + } + return res; + } + + @Override + public WorkContext getWorkContext() { + return new WorkContext() { + + @Override + public void registerXAResource(XAResource resource, String recoveryId) { + getTransaction().enlistResource(resource); + } + }; + } + + /* + * WORK TRANSACTION IMPLEMENTATION + */ + + @Override + public boolean isNoTransactionStatus() { + return tsa.getNoTransactionStatus().equals(getStatus()); + } + + /* + * JTA IMPLEMENTATION + */ + + public void begin() +// throws NotSupportedException, SystemException + { + if (getCurrent() != null) + throw new UnsupportedOperationException("Nested transactions are not supported"); + SimpleTransaction transaction = new SimpleTransaction(this, tsa); + knownTransactions.put(transaction.getXid(), transaction); + current.set(transaction); + } + + public void commit() +// throws RollbackException, HeuristicMixedException, HeuristicRollbackException, +// SecurityException, IllegalStateException, SystemException + { + if (getCurrent() == null) + throw new IllegalStateException("No transaction registered with the current thread."); + getCurrent().commit(); + } + + public int getStatus() +// throws SystemException + { + if (getCurrent() == null) + return tsa.getNoTransactionStatus(); + return getTransaction().getStatus(); + } + + public SimpleTransaction getTransaction() +// throws SystemException + { + return getCurrent(); + } + + protected SimpleTransaction getCurrent() +// throws SystemException + { + SimpleTransaction transaction = current.get(); + if (transaction == null) + return null; + Integer status = transaction.getStatus(); + if (status.equals(tsa.getCommittedStatus()) || status.equals(tsa.getRolledBackStatus())) { + current.remove(); + return null; + } + return transaction; + } + + void unregister(Xid xid) { + knownTransactions.remove(xid); + } + + public void resume(SimpleTransaction tobj) +// throws InvalidTransactionException, IllegalStateException, SystemException + { + if (getCurrent() != null) + throw new IllegalStateException("Transaction " + current.get() + " already registered"); + current.set(tobj); + } + + public void rollback() +// throws IllegalStateException, SecurityException, SystemException + { + if (getCurrent() == null) + throw new IllegalStateException("No transaction registered with the current thread."); + getCurrent().rollback(); + } + + public void setRollbackOnly() +// throws IllegalStateException, SystemException + { + if (getCurrent() == null) + throw new IllegalStateException("No transaction registered with the current thread."); + getCurrent().setRollbackOnly(); + } + + public void setTransactionTimeout(int seconds) +// throws SystemException + { + throw new UnsupportedOperationException(); + } + + public SimpleTransaction suspend() +// throws SystemException + { + SimpleTransaction transaction = getCurrent(); + current.remove(); + return transaction; + } + +// public TransactionSynchronizationRegistry getTsr() { +// return syncRegistry; +// } +// +// private class SyncRegistry implements TransactionSynchronizationRegistry { +// @Override +// public Object getTransactionKey() { +// try { +// SimpleTransaction transaction = getCurrent(); +// if (transaction == null) +// return null; +// return getCurrent().getXid(); +// } catch (SystemException e) { +// throw new IllegalStateException("Cannot get transaction key", e); +// } +// } +// +// @Override +// public void putResource(Object key, Object value) { +// throw new UnsupportedOperationException(); +// } +// +// @Override +// public Object getResource(Object key) { +// throw new UnsupportedOperationException(); +// } +// +// @Override +// public void registerInterposedSynchronization(Synchronization sync) { +// throw new UnsupportedOperationException(); +// } +// +// @Override +// public int getTransactionStatus() { +// try { +// return getStatus(); +// } catch (SystemException e) { +// throw new IllegalStateException("Cannot get status", e); +// } +// } +// +// @Override +// public boolean getRollbackOnly() { +// try { +// return getStatus() == Status.STATUS_MARKED_ROLLBACK; +// } catch (SystemException e) { +// throw new IllegalStateException("Cannot get status", e); +// } +// } +// +// @Override +// public void setRollbackOnly() { +// try { +// getCurrent().setRollbackOnly(); +// } catch (Exception e) { +// throw new IllegalStateException("Cannot set rollback only", e); +// } +// } +// +// } +} diff --git a/org.argeo.util/src/org/argeo/osgi/transaction/TransactionStatusAdapter.java b/org.argeo.util/src/org/argeo/osgi/transaction/TransactionStatusAdapter.java new file mode 100644 index 000000000..87abceba4 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/transaction/TransactionStatusAdapter.java @@ -0,0 +1,22 @@ +package org.argeo.osgi.transaction; + +/** Abstract the various approaches to represent transaction status. */ +public interface TransactionStatusAdapter { + T getActiveStatus(); + + T getPreparingStatus(); + + T getMarkedRollbackStatus(); + + T getPreparedStatus(); + + T getCommittingStatus(); + + T getCommittedStatus(); + + T getRollingBackStatus(); + + T getRolledBackStatus(); + + T getNoTransactionStatus(); +} diff --git a/org.argeo.util/src/org/argeo/osgi/transaction/UuidXid.java b/org.argeo.util/src/org/argeo/osgi/transaction/UuidXid.java new file mode 100644 index 000000000..729aef8e5 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/transaction/UuidXid.java @@ -0,0 +1,132 @@ +package org.argeo.osgi.transaction; + +import java.io.Serializable; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.UUID; + +import javax.transaction.xa.Xid; + +/** + * Implementation of {@link Xid} based on {@link UUID}, using max significant + * bits as global transaction id, and least significant bits as branch + * qualifier. + */ +public class UuidXid implements Xid, Serializable { + private static final long serialVersionUID = -5380531989917886819L; + public final static int FORMAT = (int) serialVersionUID; + + private final static int BYTES_PER_LONG = Long.SIZE / Byte.SIZE; + + private final int format; + private final byte[] globalTransactionId; + private final byte[] branchQualifier; + private final String uuid; + private final int hashCode; + + public UuidXid() { + this(UUID.randomUUID()); + } + + public UuidXid(UUID uuid) { + this.format = FORMAT; + this.globalTransactionId = uuidToBytes(uuid.getMostSignificantBits()); + this.branchQualifier = uuidToBytes(uuid.getLeastSignificantBits()); + this.uuid = uuid.toString(); + this.hashCode = uuid.hashCode(); + } + + public UuidXid(Xid xid) { + this(xid.getFormatId(), xid.getGlobalTransactionId(), xid + .getBranchQualifier()); + } + + private UuidXid(int format, byte[] globalTransactionId, + byte[] branchQualifier) { + this.format = format; + this.globalTransactionId = globalTransactionId; + this.branchQualifier = branchQualifier; + this.uuid = bytesToUUID(globalTransactionId, branchQualifier) + .toString(); + this.hashCode = uuid.hashCode(); + } + + @Override + public int getFormatId() { + return format; + } + + @Override + public byte[] getGlobalTransactionId() { + return Arrays.copyOf(globalTransactionId, globalTransactionId.length); + } + + @Override + public byte[] getBranchQualifier() { + return Arrays.copyOf(branchQualifier, branchQualifier.length); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj instanceof UuidXid) { + UuidXid that = (UuidXid) obj; + return Arrays.equals(globalTransactionId, that.globalTransactionId) + && Arrays.equals(branchQualifier, that.branchQualifier); + } + if (obj instanceof Xid) { + Xid that = (Xid) obj; + return Arrays.equals(globalTransactionId, + that.getGlobalTransactionId()) + && Arrays + .equals(branchQualifier, that.getBranchQualifier()); + } + return uuid.equals(obj.toString()); + } + + @Override + protected Object clone() throws CloneNotSupportedException { + return new UuidXid(format, globalTransactionId, branchQualifier); + } + + @Override + public String toString() { + return uuid; + } + + public UUID asUuid() { + return bytesToUUID(globalTransactionId, branchQualifier); + } + + public static byte[] uuidToBytes(long bits) { + ByteBuffer buffer = ByteBuffer.allocate(BYTES_PER_LONG); + buffer.putLong(0, bits); + return buffer.array(); + } + + public static UUID bytesToUUID(byte[] most, byte[] least) { + if (most.length < BYTES_PER_LONG) + most = Arrays.copyOf(most, BYTES_PER_LONG); + if (least.length < BYTES_PER_LONG) + least = Arrays.copyOf(least, BYTES_PER_LONG); + ByteBuffer buffer = ByteBuffer.allocate(2 * BYTES_PER_LONG); + buffer.put(most, 0, BYTES_PER_LONG); + buffer.put(least, 0, BYTES_PER_LONG); + buffer.flip(); + return new UUID(buffer.getLong(), buffer.getLong()); + } + + // public static void main(String[] args) { + // UUID uuid = UUID.randomUUID(); + // System.out.println(uuid); + // uuid = bytesToUUID(uuidToBytes(uuid.getMostSignificantBits()), + // uuidToBytes(uuid.getLeastSignificantBits())); + // System.out.println(uuid); + // } +} diff --git a/org.argeo.util/src/org/argeo/osgi/transaction/WorkContext.java b/org.argeo.util/src/org/argeo/osgi/transaction/WorkContext.java new file mode 100644 index 000000000..f50f20870 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/transaction/WorkContext.java @@ -0,0 +1,11 @@ +package org.argeo.osgi.transaction; + +import javax.transaction.xa.XAResource; + +/** + * A minimalistic interface similar to OSGi transaction context in order to + * register XA resources. + */ +public interface WorkContext { + void registerXAResource(XAResource resource, String recoveryId); +} diff --git a/org.argeo.util/src/org/argeo/osgi/transaction/WorkControl.java b/org.argeo.util/src/org/argeo/osgi/transaction/WorkControl.java new file mode 100644 index 000000000..766809555 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/transaction/WorkControl.java @@ -0,0 +1,15 @@ +package org.argeo.osgi.transaction; + +import java.util.concurrent.Callable; + +/** + * A minimalistic interface inspired by OSGi transaction control in order to + * commit units of work externally. + */ +public interface WorkControl { + T required(Callable work); + + void setRollbackOnly(); + + WorkContext getWorkContext(); +} diff --git a/org.argeo.util/src/org/argeo/osgi/transaction/WorkTransaction.java b/org.argeo.util/src/org/argeo/osgi/transaction/WorkTransaction.java new file mode 100644 index 000000000..653390912 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/transaction/WorkTransaction.java @@ -0,0 +1,15 @@ +package org.argeo.osgi.transaction; + +/** + * A minimalistic interface inspired by JTA user transaction in order to commit + * units of work externally. + */ +public interface WorkTransaction { + void begin(); + + void commit(); + + void rollback(); + + boolean isNoTransactionStatus(); +} diff --git a/org.argeo.util/src/org/argeo/osgi/transaction/package-info.java b/org.argeo.util/src/org/argeo/osgi/transaction/package-info.java new file mode 100644 index 000000000..3d3756243 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/transaction/package-info.java @@ -0,0 +1,2 @@ +/** Minimalistic and partial XA transaction manager implementation. */ +package org.argeo.osgi.transaction; \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java b/org.argeo.util/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java new file mode 100644 index 000000000..7279877e0 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java @@ -0,0 +1,509 @@ +package org.argeo.osgi.useradmin; + +import static org.argeo.naming.LdapAttrs.objectClass; +import static org.argeo.naming.LdapObjs.extensibleObject; +import static org.argeo.naming.LdapObjs.inetOrgPerson; +import static org.argeo.naming.LdapObjs.organizationalPerson; +import static org.argeo.naming.LdapObjs.person; +import static org.argeo.naming.LdapObjs.top; + +import java.io.File; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.List; + +import javax.naming.InvalidNameException; +import javax.naming.NameNotFoundException; +import javax.naming.NamingEnumeration; +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.naming.LdapAttrs; +import org.argeo.osgi.transaction.WorkControl; +import org.osgi.framework.Filter; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.service.useradmin.Authorization; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.User; +import org.osgi.service.useradmin.UserAdmin; + +/** Base class for a {@link UserDirectory}. */ +public abstract class AbstractUserDirectory implements UserAdmin, UserDirectory { + static final String SHARED_STATE_USERNAME = "javax.security.auth.login.name"; + static final String SHARED_STATE_PASSWORD = "javax.security.auth.login.password"; + + private final Hashtable properties; + private final LdapName baseDn, userBaseDn, groupBaseDn; + private final String userObjectClass, userBase, groupObjectClass, groupBase; + + private final boolean readOnly; + private final boolean disabled; + private final String uri; + + private UserAdmin externalRoles; + // private List indexedUserProperties = Arrays + // .asList(new String[] { LdapAttrs.uid.name(), LdapAttrs.mail.name(), + // LdapAttrs.cn.name() }); + + private final boolean scoped; + + private String memberAttributeId = "member"; + private List credentialAttributeIds = Arrays + .asList(new String[] { LdapAttrs.userPassword.name(), LdapAttrs.authPassword.name() }); + + // Transaction +// private TransactionManager transactionManager; + private WorkControl transactionControl; + private WcXaResource xaResource = new WcXaResource(this); + + AbstractUserDirectory(URI uriArg, Dictionary props, boolean scoped) { + this.scoped = scoped; + properties = new Hashtable(); + for (Enumeration keys = props.keys(); keys.hasMoreElements();) { + String key = keys.nextElement(); + properties.put(key, props.get(key)); + } + + if (uriArg != null) { + uri = uriArg.toString(); + // uri from properties is ignored + } else { + String uriStr = UserAdminConf.uri.getValue(properties); + if (uriStr == null) + uri = null; + else + uri = uriStr; + } + + userObjectClass = UserAdminConf.userObjectClass.getValue(properties); + userBase = UserAdminConf.userBase.getValue(properties); + groupObjectClass = UserAdminConf.groupObjectClass.getValue(properties); + groupBase = UserAdminConf.groupBase.getValue(properties); + try { + baseDn = new LdapName(UserAdminConf.baseDn.getValue(properties)); + userBaseDn = new LdapName(userBase + "," + baseDn); + groupBaseDn = new LdapName(groupBase + "," + baseDn); + } catch (InvalidNameException e) { + throw new UserDirectoryException("Badly formated base DN " + UserAdminConf.baseDn.getValue(properties), e); + } + String readOnlyStr = UserAdminConf.readOnly.getValue(properties); + if (readOnlyStr == null) { + readOnly = readOnlyDefault(uri); + properties.put(UserAdminConf.readOnly.name(), Boolean.toString(readOnly)); + } else + readOnly = Boolean.parseBoolean(readOnlyStr); + String disabledStr = UserAdminConf.disabled.getValue(properties); + if (disabledStr != null) + disabled = Boolean.parseBoolean(disabledStr); + else + disabled = false; + } + + /** Returns the groups this user is a direct member of. */ + protected abstract List getDirectGroups(LdapName dn); + + protected abstract Boolean daoHasRole(LdapName dn); + + protected abstract DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException; + + protected abstract List doGetRoles(Filter f); + + protected abstract AbstractUserDirectory scope(User user); + + public void init() { + + } + + public void destroy() { + + } + + protected boolean isEditing() { + return xaResource.wc() != null; + } + + protected UserDirectoryWorkingCopy getWorkingCopy() { + UserDirectoryWorkingCopy wc = xaResource.wc(); + if (wc == null) + return null; + return wc; + } + + protected void checkEdit() { +// Transaction transaction; +// try { +// transaction = transactionManager.getTransaction(); +// } catch (SystemException e) { +// throw new UserDirectoryException("Cannot get transaction", e); +// } +// if (transaction == null) +// throw new UserDirectoryException("A transaction needs to be active in order to edit"); + if (xaResource.wc() == null) { + try { +// transaction.enlistResource(xaResource); + transactionControl.getWorkContext().registerXAResource(xaResource, null); + } catch (Exception e) { + throw new UserDirectoryException("Cannot enlist " + xaResource, e); + } + } else { + } + } + + protected List getAllRoles(DirectoryUser user) { + List allRoles = new ArrayList(); + if (user != null) { + collectRoles(user, allRoles); + allRoles.add(user); + } else + collectAnonymousRoles(allRoles); + return allRoles; + } + + private void collectRoles(DirectoryUser user, List allRoles) { + Attributes attrs = user.getAttributes(); + // TODO centralize attribute name + Attribute memberOf = attrs.get(LdapAttrs.memberOf.name()); + // if user belongs to this directory, we only check meberOf + if (memberOf != null && user.getDn().startsWith(getBaseDn())) { + try { + NamingEnumeration values = memberOf.getAll(); + while (values.hasMore()) { + Object value = values.next(); + LdapName groupDn = new LdapName(value.toString()); + DirectoryUser group = doGetRole(groupDn); + if (group != null) + allRoles.add(group); + } + } catch (Exception e) { + throw new UserDirectoryException("Cannot get memberOf groups for " + user, e); + } + } else { + for (LdapName groupDn : getDirectGroups(user.getDn())) { + // TODO check for loops + DirectoryUser group = doGetRole(groupDn); + if (group != null) { + allRoles.add(group); + collectRoles(group, allRoles); + } + } + } + } + + private void collectAnonymousRoles(List allRoles) { + // TODO gather anonymous roles + } + + // USER ADMIN + @Override + public Role getRole(String name) { + return doGetRole(toDn(name)); + } + + protected DirectoryUser doGetRole(LdapName dn) { + UserDirectoryWorkingCopy wc = getWorkingCopy(); + DirectoryUser user; + try { + user = daoGetRole(dn); + } catch (NameNotFoundException e) { + user = null; + } + if (wc != null) { + if (user == null && wc.getNewUsers().containsKey(dn)) + user = wc.getNewUsers().get(dn); + else if (wc.getDeletedUsers().containsKey(dn)) + user = null; + } + return user; + } + + @Override + public Role[] getRoles(String filter) throws InvalidSyntaxException { + UserDirectoryWorkingCopy wc = getWorkingCopy(); + Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null; + List res = doGetRoles(f); + if (wc != null) { + for (Iterator it = res.iterator(); it.hasNext();) { + DirectoryUser user = it.next(); + LdapName dn = user.getDn(); + if (wc.getDeletedUsers().containsKey(dn)) + it.remove(); + } + for (DirectoryUser user : wc.getNewUsers().values()) { + if (f == null || f.match(user.getProperties())) + res.add(user); + } + // no need to check modified users, + // since doGetRoles was already based on the modified attributes + } + return res.toArray(new Role[res.size()]); + } + + @Override + public User getUser(String key, String value) { + // TODO check value null or empty + List collectedUsers = new ArrayList(); + if (key != null) { + doGetUser(key, value, collectedUsers); + } else { + throw new UserDirectoryException("Key cannot be null"); + } + + if (collectedUsers.size() == 1) { + return collectedUsers.get(0); + } else if (collectedUsers.size() > 1) { + // log.warn(collectedUsers.size() + " users for " + (key != null ? key + "=" : + // "") + value); + } + return null; + } + + protected void doGetUser(String key, String value, List collectedUsers) { + try { + Filter f = FrameworkUtil.createFilter("(" + key + "=" + value + ")"); + List users = doGetRoles(f); + collectedUsers.addAll(users); + } catch (InvalidSyntaxException e) { + throw new UserDirectoryException("Cannot get user with " + key + "=" + value, e); + } + } + + @Override + public Authorization getAuthorization(User user) { + if (user == null || user instanceof DirectoryUser) { + return new LdifAuthorization(user, getAllRoles((DirectoryUser) user)); + } else { + // bind + AbstractUserDirectory scopedUserAdmin = scope(user); + try { + DirectoryUser directoryUser = (DirectoryUser) scopedUserAdmin.getRole(user.getName()); + if (directoryUser == null) + throw new UserDirectoryException("No scoped user found for " + user); + LdifAuthorization authorization = new LdifAuthorization(directoryUser, + scopedUserAdmin.getAllRoles(directoryUser)); + return authorization; + } finally { + scopedUserAdmin.destroy(); + } + } + } + + @Override + public Role createRole(String name, int type) { + checkEdit(); + UserDirectoryWorkingCopy wc = getWorkingCopy(); + LdapName dn = toDn(name); + if ((daoHasRole(dn) && !wc.getDeletedUsers().containsKey(dn)) || wc.getNewUsers().containsKey(dn)) + throw new UserDirectoryException("Already a role " + name); + BasicAttributes attrs = new BasicAttributes(true); + // attrs.put(LdifName.dn.name(), dn.toString()); + Rdn nameRdn = dn.getRdn(dn.size() - 1); + // TODO deal with multiple attr RDN + attrs.put(nameRdn.getType(), nameRdn.getValue()); + if (wc.getDeletedUsers().containsKey(dn)) { + wc.getDeletedUsers().remove(dn); + wc.getModifiedUsers().put(dn, attrs); + return getRole(name); + } else { + wc.getModifiedUsers().put(dn, attrs); + DirectoryUser newRole = newRole(dn, type, attrs); + wc.getNewUsers().put(dn, newRole); + return newRole; + } + } + + protected DirectoryUser newRole(LdapName dn, int type, Attributes attrs) { + LdifUser newRole; + BasicAttribute objClass = new BasicAttribute(objectClass.name()); + if (type == Role.USER) { + String userObjClass = newUserObjectClass(dn); + objClass.add(userObjClass); + if (inetOrgPerson.name().equals(userObjClass)) { + objClass.add(organizationalPerson.name()); + objClass.add(person.name()); + } else if (organizationalPerson.name().equals(userObjClass)) { + objClass.add(person.name()); + } + objClass.add(top.name()); + objClass.add(extensibleObject.name()); + attrs.put(objClass); + newRole = new LdifUser(this, dn, attrs); + } else if (type == Role.GROUP) { + String groupObjClass = getGroupObjectClass(); + objClass.add(groupObjClass); + // objClass.add(LdifName.extensibleObject.name()); + objClass.add(top.name()); + attrs.put(objClass); + newRole = new LdifGroup(this, dn, attrs); + } else + throw new UserDirectoryException("Unsupported type " + type); + return newRole; + } + + @Override + public boolean removeRole(String name) { + checkEdit(); + UserDirectoryWorkingCopy wc = getWorkingCopy(); + LdapName dn = toDn(name); + boolean actuallyDeleted; + if (daoHasRole(dn) || wc.getNewUsers().containsKey(dn)) { + DirectoryUser user = (DirectoryUser) getRole(name); + wc.getDeletedUsers().put(dn, user); + actuallyDeleted = true; + } else {// just removing from groups (e.g. system roles) + actuallyDeleted = false; + } + for (LdapName groupDn : getDirectGroups(dn)) { + DirectoryUser group = doGetRole(groupDn); + group.getAttributes().get(getMemberAttributeId()).remove(dn.toString()); + } + return actuallyDeleted; + } + + // TRANSACTION + protected void prepare(UserDirectoryWorkingCopy wc) { + + } + + protected void commit(UserDirectoryWorkingCopy wc) { + + } + + protected void rollback(UserDirectoryWorkingCopy wc) { + + } + + // UTILITIES + protected LdapName toDn(String name) { + try { + return new LdapName(name); + } catch (InvalidNameException e) { + throw new UserDirectoryException("Badly formatted name", e); + } + } + + // GETTERS + protected String getMemberAttributeId() { + return memberAttributeId; + } + + protected List getCredentialAttributeIds() { + return credentialAttributeIds; + } + + protected String getUri() { + return uri; + } + + private static boolean readOnlyDefault(String uriStr) { + if (uriStr == null) + return true; + /// TODO make it more generic + URI uri; + try { + uri = new URI(uriStr.split(" ")[0]); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + if (uri.getScheme() == null) + return false;// assume relative file to be writable + if (uri.getScheme().equals(UserAdminConf.SCHEME_FILE)) { + File file = new File(uri); + if (file.exists()) + return !file.canWrite(); + else + return !file.getParentFile().canWrite(); + } else if (uri.getScheme().equals(UserAdminConf.SCHEME_LDAP)) { + if (uri.getAuthority() != null)// assume writable if authenticated + return false; + } else if (uri.getScheme().equals(UserAdminConf.SCHEME_OS)) { + return true; + } + return true;// read only by default + } + + public boolean isReadOnly() { + return readOnly; + } + + public boolean isDisabled() { + return disabled; + } + + protected UserAdmin getExternalRoles() { + return externalRoles; + } + + protected int roleType(LdapName dn) { + if (dn.startsWith(groupBaseDn)) + return Role.GROUP; + else if (dn.startsWith(userBaseDn)) + return Role.USER; + else + return Role.GROUP; + } + + /** dn can be null, in that case a default should be returned. */ + public String getUserObjectClass() { + return userObjectClass; + } + + public String getUserBase() { + return userBase; + } + + protected String newUserObjectClass(LdapName dn) { + return getUserObjectClass(); + } + + public String getGroupObjectClass() { + return groupObjectClass; + } + + public String getGroupBase() { + return groupBase; + } + + public LdapName getBaseDn() { + return (LdapName) baseDn.clone(); + } + + public Dictionary getProperties() { + return properties; + } + + public Dictionary cloneProperties() { + return new Hashtable<>(properties); + } + + public void setExternalRoles(UserAdmin externalRoles) { + this.externalRoles = externalRoles; + } + +// public void setTransactionManager(TransactionManager transactionManager) { +// this.transactionManager = transactionManager; +// } + + public void setTransactionControl(WorkControl transactionControl) { + this.transactionControl = transactionControl; + } + + public WcXaResource getXaResource() { + return xaResource; + } + + public boolean isScoped() { + return scoped; + } + +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingAuthorization.java b/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingAuthorization.java new file mode 100644 index 000000000..05ba94889 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingAuthorization.java @@ -0,0 +1,77 @@ +package org.argeo.osgi.useradmin; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.security.auth.x500.X500Principal; + +import org.osgi.service.useradmin.Authorization; + +/** An {@link Authorization} which combines roles form various auth sources. */ +class AggregatingAuthorization implements Authorization { + private final String name; + private final String displayName; + private final Set systemRoles; + private final Set roles; + + public AggregatingAuthorization(String name, String displayName, Set systemRoles, String[] roles) { + this.name = new X500Principal(name).getName(); + this.displayName = displayName; + this.systemRoles = Collections.unmodifiableSet(new HashSet<>(systemRoles)); + Set temp = new HashSet<>(); + for (String role : roles) { + if (!temp.contains(role)) + temp.add(role); + } + this.roles = Collections.unmodifiableSet(temp); + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean hasRole(String name) { + if (systemRoles.contains(name)) + return true; + if (roles.contains(name)) + return true; + return false; + } + + @Override + public String[] getRoles() { + int size = systemRoles.size() + roles.size(); + List res = new ArrayList(size); + res.addAll(systemRoles); + res.addAll(roles); + return res.toArray(new String[size]); + } + + @Override + public int hashCode() { + if (name == null) + return super.hashCode(); + return name.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Authorization)) + return false; + Authorization that = (Authorization) obj; + if (name == null) + return that.getName() == null; + return name.equals(that.getName()); + } + + @Override + public String toString() { + return displayName; + } + +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java b/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java new file mode 100644 index 000000000..bee513546 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java @@ -0,0 +1,276 @@ +package org.argeo.osgi.useradmin; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; + +import org.osgi.framework.InvalidSyntaxException; +import org.osgi.service.useradmin.Authorization; +import org.osgi.service.useradmin.Group; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.User; +import org.osgi.service.useradmin.UserAdmin; + +/** + * Aggregates multiple {@link UserDirectory} and integrates them with system + * roles. + */ +public class AggregatingUserAdmin implements UserAdmin { + private final LdapName systemRolesBaseDn; + private final LdapName tokensBaseDn; + + // DAOs + private AbstractUserDirectory systemRoles = null; + private AbstractUserDirectory tokens = null; + private Map businessRoles = new HashMap(); + + public AggregatingUserAdmin(String systemRolesBaseDn, String tokensBaseDn) { + try { + this.systemRolesBaseDn = new LdapName(systemRolesBaseDn); + if (tokensBaseDn != null) + this.tokensBaseDn = new LdapName(tokensBaseDn); + else + this.tokensBaseDn = null; + } catch (InvalidNameException e) { + throw new UserDirectoryException("Cannot initialize " + AggregatingUserAdmin.class, e); + } + } + + @Override + public Role createRole(String name, int type) { + return findUserAdmin(name).createRole(name, type); + } + + @Override + public boolean removeRole(String name) { + boolean actuallyDeleted = findUserAdmin(name).removeRole(name); + systemRoles.removeRole(name); + return actuallyDeleted; + } + + @Override + public Role getRole(String name) { + return findUserAdmin(name).getRole(name); + } + + @Override + public Role[] getRoles(String filter) throws InvalidSyntaxException { + List res = new ArrayList(); + for (UserAdmin userAdmin : businessRoles.values()) { + res.addAll(Arrays.asList(userAdmin.getRoles(filter))); + } + res.addAll(Arrays.asList(systemRoles.getRoles(filter))); + return res.toArray(new Role[res.size()]); + } + + @Override + public User getUser(String key, String value) { + List res = new ArrayList(); + for (UserAdmin userAdmin : businessRoles.values()) { + User u = userAdmin.getUser(key, value); + if (u != null) + res.add(u); + } + // Note: node roles cannot contain users, so it is not searched + return res.size() == 1 ? res.get(0) : null; + } + + @Override + public Authorization getAuthorization(User user) { + if (user == null) {// anonymous + return systemRoles.getAuthorization(null); + } + AbstractUserDirectory userReferentialOfThisUser = findUserAdmin(user.getName()); + Authorization rawAuthorization = userReferentialOfThisUser.getAuthorization(user); + String usernameToUse; + String displayNameToUse; + if (user instanceof Group) { + // TODO check whether this is still working + String ownerDn = TokenUtils.userDn((Group) user); + if (ownerDn != null) {// tokens + UserAdmin ownerUserAdmin = findUserAdmin(ownerDn); + User ownerUser = (User) ownerUserAdmin.getRole(ownerDn); + usernameToUse = ownerDn; + displayNameToUse = LdifAuthorization.extractDisplayName(ownerUser); + } else { + usernameToUse = rawAuthorization.getName(); + displayNameToUse = rawAuthorization.toString(); + } + } else {// regular users + usernameToUse = rawAuthorization.getName(); + displayNameToUse = rawAuthorization.toString(); + } + + // gather roles from other referentials + final AbstractUserDirectory userAdminToUse;// possibly scoped when authenticating + if (user instanceof DirectoryUser) { + userAdminToUse = userReferentialOfThisUser; + } else if (user instanceof AuthenticatingUser) { + userAdminToUse = userReferentialOfThisUser.scope(user); + } else { + throw new IllegalArgumentException("Unsupported user type " + user.getClass()); + } + + try { + Set sysRoles = new HashSet(); + for (String role : rawAuthorization.getRoles()) { + User userOrGroup = (User) userAdminToUse.getRole(role); + Authorization auth = systemRoles.getAuthorization(userOrGroup); + systemRoles: for (String systemRole : auth.getRoles()) { + if (role.equals(systemRole)) + continue systemRoles; + sysRoles.add(systemRole); + } +// sysRoles.addAll(Arrays.asList(auth.getRoles())); + } + addAbstractSystemRoles(rawAuthorization, sysRoles); + Authorization authorization = new AggregatingAuthorization(usernameToUse, displayNameToUse, sysRoles, + rawAuthorization.getRoles()); + return authorization; + } finally { + if (userAdminToUse != null && userAdminToUse.isScoped()) { + userAdminToUse.destroy(); + } + } + } + + /** + * Enrich with application-specific roles which are strictly programmatic, such + * as anonymous/user semantics. + */ + protected void addAbstractSystemRoles(Authorization rawAuthorization, Set sysRoles) { + + } + + // + // USER ADMIN AGGREGATOR + // + protected void addUserDirectory(AbstractUserDirectory userDirectory) { + LdapName baseDn = userDirectory.getBaseDn(); + if (isSystemRolesBaseDn(baseDn)) { + this.systemRoles = userDirectory; + systemRoles.setExternalRoles(this); + } else if (isTokensBaseDn(baseDn)) { + this.tokens = userDirectory; + tokens.setExternalRoles(this); + } else { + if (businessRoles.containsKey(baseDn)) + throw new UserDirectoryException("There is already a user admin for " + baseDn); + businessRoles.put(baseDn, userDirectory); + } + userDirectory.init(); + postAdd(userDirectory); + } + + /** Called after a new user directory has been added */ + protected void postAdd(AbstractUserDirectory userDirectory) { + } + +// private UserAdmin findUserAdmin(User user) { +// if (user == null) +// throw new IllegalArgumentException("User should not be null"); +// AbstractUserDirectory userAdmin = findUserAdmin(user.getName()); +// if (user instanceof DirectoryUser) { +// return userAdmin; +// } else { +// return userAdmin.scope(user); +// } +// } + + private AbstractUserDirectory findUserAdmin(String name) { + try { + return findUserAdmin(new LdapName(name)); + } catch (InvalidNameException e) { + throw new UserDirectoryException("Badly formatted name " + name, e); + } + } + + private AbstractUserDirectory findUserAdmin(LdapName name) { + if (name.startsWith(systemRolesBaseDn)) + return systemRoles; + if (tokensBaseDn != null && name.startsWith(tokensBaseDn)) + return tokens; + List res = new ArrayList<>(1); + userDirectories: for (LdapName baseDn : businessRoles.keySet()) { + AbstractUserDirectory userDirectory = businessRoles.get(baseDn); + if (name.startsWith(baseDn)) { + if (userDirectory.isDisabled()) + continue userDirectories; +// if (res.isEmpty()) { + res.add(userDirectory); +// } else { +// for (AbstractUserDirectory ud : res) { +// LdapName bd = ud.getBaseDn(); +// if (userDirectory.getBaseDn().startsWith(bd)) { +// // child user directory +// } +// } +// } + } + } + if (res.size() == 0) + throw new UserDirectoryException("Cannot find user admin for " + name); + if (res.size() > 1) + throw new UserDirectoryException("Multiple user admin found for " + name); + return res.get(0); + } + + protected boolean isSystemRolesBaseDn(LdapName baseDn) { + return baseDn.equals(systemRolesBaseDn); + } + + protected boolean isTokensBaseDn(LdapName baseDn) { + return tokensBaseDn != null && baseDn.equals(tokensBaseDn); + } + +// protected Dictionary currentState() { +// Dictionary res = new Hashtable(); +// // res.put(NodeConstants.CN, NodeConstants.DEFAULT); +// for (LdapName name : businessRoles.keySet()) { +// AbstractUserDirectory userDirectory = businessRoles.get(name); +// String uri = UserAdminConf.propertiesAsUri(userDirectory.getProperties()).toString(); +// res.put(uri, ""); +// } +// return res; +// } + + public void destroy() { + for (LdapName name : businessRoles.keySet()) { + AbstractUserDirectory userDirectory = businessRoles.get(name); + destroy(userDirectory); + } + businessRoles.clear(); + businessRoles = null; + destroy(systemRoles); + systemRoles = null; + } + + private void destroy(AbstractUserDirectory userDirectory) { + preDestroy(userDirectory); + userDirectory.destroy(); + } + + protected void removeUserDirectory(LdapName baseDn) { + if (isSystemRolesBaseDn(baseDn)) + throw new UserDirectoryException("System roles cannot be removed "); + if (!businessRoles.containsKey(baseDn)) + throw new UserDirectoryException("No user directory registered for " + baseDn); + AbstractUserDirectory userDirectory = businessRoles.remove(baseDn); + destroy(userDirectory); + } + + /** + * Called before each user directory is destroyed, so that additional actions + * can be performed. + */ + protected void preDestroy(AbstractUserDirectory userDirectory) { + } + +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/AuthenticatingUser.java b/org.argeo.util/src/org/argeo/osgi/useradmin/AuthenticatingUser.java new file mode 100644 index 000000000..01db8be98 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/AuthenticatingUser.java @@ -0,0 +1,82 @@ +package org.argeo.osgi.useradmin; + +import java.util.Dictionary; +import java.util.Hashtable; + +import javax.naming.ldap.LdapName; + +import org.osgi.service.useradmin.User; + +/** + * A special user type used during authentication in order to provide the + * credentials required for scoping the user admin. + */ +public class AuthenticatingUser implements User { + /** From com.sun.security.auth.module.*LoginModule */ + public final static String SHARED_STATE_NAME = "javax.security.auth.login.name"; + /** From com.sun.security.auth.module.*LoginModule */ + public final static String SHARED_STATE_PWD = "javax.security.auth.login.password"; + + private final String name; + private final Dictionary credentials; + + public AuthenticatingUser(LdapName name) { + if (name == null) + throw new NullPointerException("Provided name cannot be null."); + this.name = name.toString(); + this.credentials = new Hashtable<>(); + } + + public AuthenticatingUser(String name, Dictionary credentials) { + this.name = name; + this.credentials = credentials; + } + + public AuthenticatingUser(String name, char[] password) { + if (name == null) + throw new NullPointerException("Provided name cannot be null."); + this.name = name; + credentials = new Hashtable<>(); + credentials.put(SHARED_STATE_NAME, name); + byte[] pwd = DigestUtils.charsToBytes(password); + credentials.put(SHARED_STATE_PWD, pwd); + } + + @Override + public String getName() { + return name; + } + + @Override + public int getType() { + return User.USER; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public Dictionary getProperties() { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + @Override + public Dictionary getCredentials() { + return credentials; + } + + @Override + public boolean hasCredential(String key, Object value) { + throw new UnsupportedOperationException(); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + + @Override + public String toString() { + return "Authenticating user " + name; + } + +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/DigestUtils.java b/org.argeo.util/src/org/argeo/osgi/useradmin/DigestUtils.java new file mode 100644 index 000000000..511c2fede --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/DigestUtils.java @@ -0,0 +1,111 @@ +package org.argeo.osgi.useradmin; + +import java.math.BigInteger; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.spec.KeySpec; +import java.util.Arrays; + +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +/** Utilities around digests, mostly those related to passwords. */ +class DigestUtils { + final static String PASSWORD_SCHEME_SHA = "SHA"; + final static String PASSWORD_SCHEME_PBKDF2_SHA256 = "PBKDF2_SHA256"; + + static byte[] sha1(byte[] bytes) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA1"); + digest.update(bytes); + byte[] checksum = digest.digest(); + return checksum; + } catch (Exception e) { + throw new UserDirectoryException("Cannot SHA1 digest", e); + } + } + + static byte[] toPasswordScheme(String passwordScheme, char[] password, byte[] salt, Integer iterations, + Integer keyLength) { + try { + if (PASSWORD_SCHEME_SHA.equals(passwordScheme)) { + MessageDigest digest = MessageDigest.getInstance("SHA1"); + byte[] bytes = charsToBytes(password); + digest.update(bytes); + return digest.digest(); + } else if (PASSWORD_SCHEME_PBKDF2_SHA256.equals(passwordScheme)) { + KeySpec spec = new PBEKeySpec(password, salt, iterations, keyLength); + + SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256"); + final int ITERATION_LENGTH = 4; + byte[] key = f.generateSecret(spec).getEncoded(); + byte[] result = new byte[ITERATION_LENGTH + salt.length + key.length]; + byte iterationsArr[] = new BigInteger(iterations.toString()).toByteArray(); + if (iterationsArr.length < ITERATION_LENGTH) { + Arrays.fill(result, 0, ITERATION_LENGTH - iterationsArr.length, (byte) 0); + System.arraycopy(iterationsArr, 0, result, ITERATION_LENGTH - iterationsArr.length, + iterationsArr.length); + } else { + System.arraycopy(iterationsArr, 0, result, 0, ITERATION_LENGTH); + } + System.arraycopy(salt, 0, result, ITERATION_LENGTH, salt.length); + System.arraycopy(key, 0, result, ITERATION_LENGTH + salt.length, key.length); + return result; + } else { + throw new UnsupportedOperationException("Unkown password scheme " + passwordScheme); + } + } catch (Exception e) { + throw new UserDirectoryException("Cannot digest", e); + } + } + + static char[] bytesToChars(Object obj) { + if (obj instanceof char[]) + return (char[]) obj; + if (!(obj instanceof byte[])) + throw new IllegalArgumentException(obj.getClass() + " is not a byte array"); + ByteBuffer fromBuffer = ByteBuffer.wrap((byte[]) obj); + CharBuffer toBuffer = StandardCharsets.UTF_8.decode(fromBuffer); + char[] res = Arrays.copyOfRange(toBuffer.array(), toBuffer.position(), toBuffer.limit()); + // Arrays.fill(fromBuffer.array(), (byte) 0); // clear sensitive data + // Arrays.fill((byte[]) obj, (byte) 0); // clear sensitive data + // Arrays.fill(toBuffer.array(), '\u0000'); // clear sensitive data + return res; + } + + static byte[] charsToBytes(char[] chars) { + CharBuffer charBuffer = CharBuffer.wrap(chars); + ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer); + byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()); + // Arrays.fill(charBuffer.array(), '\u0000'); // clear sensitive data + // Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data + return bytes; + } + + static String sha1str(String str) { + byte[] hash = sha1(str.getBytes(StandardCharsets.UTF_8)); + return encodeHexString(hash); + } + + final private static char[] hexArray = "0123456789abcdef".toCharArray(); + + /** + * From + * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to + * -a-hex-string-in-java + */ + public static String encodeHexString(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + private DigestUtils() { + } +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryGroup.java b/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryGroup.java new file mode 100644 index 000000000..7f8046313 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryGroup.java @@ -0,0 +1,12 @@ +package org.argeo.osgi.useradmin; + +import java.util.List; + +import javax.naming.ldap.LdapName; + +import org.osgi.service.useradmin.Group; + +/** A group in a user directroy. */ +interface DirectoryGroup extends Group, DirectoryUser { + List getMemberNames(); +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUser.java b/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUser.java new file mode 100644 index 000000000..146b80578 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/DirectoryUser.java @@ -0,0 +1,15 @@ +package org.argeo.osgi.useradmin; + +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; + +import org.osgi.service.useradmin.User; + +/** A user in a user directory. */ +interface DirectoryUser extends User { + LdapName getDn(); + + Attributes getAttributes(); + + void publishAttributes(Attributes modifiedAttributes); +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/IpaUtils.java b/org.argeo.util/src/org/argeo/osgi/useradmin/IpaUtils.java new file mode 100644 index 000000000..d56c06ac0 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/IpaUtils.java @@ -0,0 +1,137 @@ +package org.argeo.osgi.useradmin; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; + +import javax.naming.InvalidNameException; +import javax.naming.NamingException; +import javax.naming.ldap.LdapName; + +import org.argeo.naming.DnsBrowser; +import org.argeo.naming.LdapAttrs; + +/** Free IPA specific conventions. */ +public class IpaUtils { + public final static String IPA_USER_BASE = "cn=users,cn=accounts"; + public final static String IPA_GROUP_BASE = "cn=groups,cn=accounts"; + public final static String IPA_SERVICE_BASE = "cn=services,cn=accounts"; + + private final static String KRB_PRINCIPAL_NAME = LdapAttrs.krbPrincipalName.name().toLowerCase(); + + public final static String IPA_USER_DIRECTORY_CONFIG = UserAdminConf.userBase + "=" + IPA_USER_BASE + "&" + + UserAdminConf.groupBase + "=" + IPA_GROUP_BASE + "&" + UserAdminConf.readOnly + "=true"; + + @Deprecated + static String domainToUserDirectoryConfigPath(String realm) { + return domainToBaseDn(realm) + "?" + IPA_USER_DIRECTORY_CONFIG + "&" + UserAdminConf.realm.name() + "=" + realm; + } + + public static void addIpaConfig(String realm, Dictionary properties) { + properties.put(UserAdminConf.baseDn.name(), domainToBaseDn(realm)); + properties.put(UserAdminConf.realm.name(), realm); + properties.put(UserAdminConf.userBase.name(), IPA_USER_BASE); + properties.put(UserAdminConf.groupBase.name(), IPA_GROUP_BASE); + properties.put(UserAdminConf.readOnly.name(), Boolean.TRUE.toString()); + } + + public static String domainToBaseDn(String domain) { + String[] dcs = domain.split("\\."); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < dcs.length; i++) { + if (i != 0) + sb.append(','); + String dc = dcs[i]; + sb.append(LdapAttrs.dc.name()).append('=').append(dc.toLowerCase()); + } + return sb.toString(); + } + + public static LdapName kerberosToDn(String kerberosName) { + String[] kname = kerberosName.split("@"); + String username = kname[0]; + String baseDn = domainToBaseDn(kname[1]); + String dn; + if (!username.contains("/")) + dn = LdapAttrs.uid + "=" + username + "," + IPA_USER_BASE + "," + baseDn; + else + dn = KRB_PRINCIPAL_NAME + "=" + kerberosName + "," + IPA_SERVICE_BASE + "," + baseDn; + try { + return new LdapName(dn); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Badly formatted name for " + kerberosName + ": " + dn); + } + } + + private IpaUtils() { + + } + + public static String kerberosDomainFromDns() { + String kerberosDomain; + try (DnsBrowser dnsBrowser = new DnsBrowser()) { + InetAddress localhost = InetAddress.getLocalHost(); + String hostname = localhost.getHostName(); + String dnsZone = hostname.substring(hostname.indexOf('.') + 1); + kerberosDomain = dnsBrowser.getRecord("_kerberos." + dnsZone, "TXT"); + return kerberosDomain; + } catch (Exception e) { + throw new UserDirectoryException("Cannot determine Kerberos domain from DNS", e); + } + + } + + public static Dictionary convertIpaUri(URI uri) { + String path = uri.getPath(); + String kerberosRealm; + if (path == null || path.length() <= 1) { + kerberosRealm = kerberosDomainFromDns(); + } else { + kerberosRealm = path.substring(1); + } + + if (kerberosRealm == null) + throw new UserDirectoryException("No Kerberos domain available for " + uri); + // TODO intergrate CA certificate in truststore + // String schemeToUse = SCHEME_LDAPS; + String schemeToUse = UserAdminConf.SCHEME_LDAP; + List ldapHosts; + String ldapHostsStr = uri.getHost(); + if (ldapHostsStr == null || ldapHostsStr.trim().equals("")) { + try (DnsBrowser dnsBrowser = new DnsBrowser()) { + ldapHosts = dnsBrowser.getSrvRecordsAsHosts("_ldap._tcp." + kerberosRealm.toLowerCase(), + schemeToUse.equals(UserAdminConf.SCHEME_LDAP) ? true : false); + if (ldapHosts == null || ldapHosts.size() == 0) { + throw new UserDirectoryException("Cannot configure LDAP for IPA " + uri); + } else { + ldapHostsStr = ldapHosts.get(0); + } + } catch (NamingException | IOException e) { + throw new UserDirectoryException("cannot convert IPA uri " + uri, e); + } + } else { + ldapHosts = new ArrayList<>(); + ldapHosts.add(ldapHostsStr); + } + + StringBuilder uriStr = new StringBuilder(); + try { + for (String host : ldapHosts) { + URI convertedUri = new URI(schemeToUse + "://" + host + "/"); + uriStr.append(convertedUri).append(' '); + } + } catch (URISyntaxException e) { + throw new UserDirectoryException("cannot convert IPA uri " + uri, e); + } + + Hashtable res = new Hashtable<>(); + res.put(UserAdminConf.uri.name(), uriStr.toString()); + addIpaConfig(kerberosRealm, res); + return res; + } +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdapConnection.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdapConnection.java new file mode 100644 index 000000000..3e869f3b0 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdapConnection.java @@ -0,0 +1,147 @@ +package org.argeo.osgi.useradmin; + +import java.util.Dictionary; +import java.util.Hashtable; + +import javax.naming.CommunicationException; +import javax.naming.Context; +import javax.naming.NameNotFoundException; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.InitialLdapContext; +import javax.naming.ldap.LdapName; + +import org.argeo.naming.LdapAttrs; + +/** A synchronized wrapper for a single {@link InitialLdapContext}. */ +// TODO implement multiple contexts and connection pooling. +class LdapConnection { + private InitialLdapContext initialLdapContext = null; + + LdapConnection(String url, Dictionary properties) { + try { + Hashtable connEnv = new Hashtable(); + connEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); + connEnv.put(Context.PROVIDER_URL, url); + connEnv.put("java.naming.ldap.attributes.binary", LdapAttrs.userPassword.name()); + // use pooling in order to avoid connection timeout +// connEnv.put("com.sun.jndi.ldap.connect.pool", "true"); +// connEnv.put("com.sun.jndi.ldap.connect.pool.timeout", 300000); + + initialLdapContext = new InitialLdapContext(connEnv, null); + // StartTlsResponse tls = (StartTlsResponse) ctx + // .extendedOperation(new StartTlsRequest()); + // tls.negotiate(); + Object securityAuthentication = properties.get(Context.SECURITY_AUTHENTICATION); + if (securityAuthentication != null) + initialLdapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, securityAuthentication); + else + initialLdapContext.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple"); + Object principal = properties.get(Context.SECURITY_PRINCIPAL); + if (principal != null) { + initialLdapContext.addToEnvironment(Context.SECURITY_PRINCIPAL, principal.toString()); + Object creds = properties.get(Context.SECURITY_CREDENTIALS); + if (creds != null) { + initialLdapContext.addToEnvironment(Context.SECURITY_CREDENTIALS, creds.toString()); + } + } + } catch (Exception e) { + throw new UserDirectoryException("Cannot connect to LDAP", e); + } + + } + + public void init() { + + } + + public void destroy() { + try { + // tls.close(); + initialLdapContext.close(); + initialLdapContext = null; + } catch (NamingException e) { + e.printStackTrace(); + } + } + + protected InitialLdapContext getLdapContext() { + return initialLdapContext; + } + + protected void reconnect() throws NamingException { + initialLdapContext.reconnect(initialLdapContext.getConnectControls()); + } + + public synchronized NamingEnumeration search(LdapName searchBase, String searchFilter, + SearchControls searchControls) throws NamingException { + NamingEnumeration results; + try { + results = getLdapContext().search(searchBase, searchFilter, searchControls); + } catch (CommunicationException e) { + reconnect(); + results = getLdapContext().search(searchBase, searchFilter, searchControls); + } + return results; + } + + public synchronized Attributes getAttributes(LdapName name) throws NamingException { + try { + return getLdapContext().getAttributes(name); + } catch (CommunicationException e) { + reconnect(); + return getLdapContext().getAttributes(name); + } + } + + synchronized void prepareChanges(UserDirectoryWorkingCopy wc) throws NamingException { + // make sure connection will work + reconnect(); + + // delete + for (LdapName dn : wc.getDeletedUsers().keySet()) { + if (!entryExists(dn)) + throw new UserDirectoryException("User to delete no found " + dn); + } + // add + for (LdapName dn : wc.getNewUsers().keySet()) { + if (entryExists(dn)) + throw new UserDirectoryException("User to create found " + dn); + } + // modify + for (LdapName dn : wc.getModifiedUsers().keySet()) { + if (!wc.getNewUsers().containsKey(dn) && !entryExists(dn)) + throw new UserDirectoryException("User to modify not found " + dn); + } + + } + + protected boolean entryExists(LdapName dn) throws NamingException { + try { + return getAttributes(dn).size() != 0; + } catch (NameNotFoundException e) { + return false; + } + } + + synchronized void commitChanges(UserDirectoryWorkingCopy wc) throws NamingException { + // delete + for (LdapName dn : wc.getDeletedUsers().keySet()) { + getLdapContext().destroySubcontext(dn); + } + // add + for (LdapName dn : wc.getNewUsers().keySet()) { + DirectoryUser user = wc.getNewUsers().get(dn); + getLdapContext().createSubcontext(dn, user.getAttributes()); + } + // modify + for (LdapName dn : wc.getModifiedUsers().keySet()) { + Attributes modifiedAttrs = wc.getModifiedUsers().get(dn); + getLdapContext().modifyAttributes(dn, DirContext.REPLACE_ATTRIBUTE, modifiedAttrs); + } + } +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdapUserAdmin.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdapUserAdmin.java new file mode 100644 index 000000000..cd28748f5 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdapUserAdmin.java @@ -0,0 +1,189 @@ +package org.argeo.osgi.useradmin; + +import static org.argeo.naming.LdapAttrs.objectClass; + +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.List; + +import javax.naming.AuthenticationNotSupportedException; +import javax.naming.Binding; +import javax.naming.Context; +import javax.naming.InvalidNameException; +import javax.naming.NameNotFoundException; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.SearchControls; +import javax.naming.directory.SearchResult; +import javax.naming.ldap.LdapName; + +import org.osgi.framework.Filter; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.User; + +/** A user admin based on a LDAP server. */ +public class LdapUserAdmin extends AbstractUserDirectory { + private LdapConnection ldapConnection; + + public LdapUserAdmin(Dictionary properties) { + this(properties, false); + } + + public LdapUserAdmin(Dictionary properties, boolean scoped) { + super(null, properties, scoped); + ldapConnection = new LdapConnection(getUri().toString(), properties); + } + + public void destroy() { + ldapConnection.destroy(); + } + + @Override + protected AbstractUserDirectory scope(User user) { + Dictionary credentials = user.getCredentials(); + String username = (String) credentials.get(SHARED_STATE_USERNAME); + if (username == null) + username = user.getName(); + Dictionary properties = cloneProperties(); + properties.put(Context.SECURITY_PRINCIPAL, username.toString()); + Object pwdCred = credentials.get(SHARED_STATE_PASSWORD); + byte[] pwd = (byte[]) pwdCred; + if (pwd != null) { + char[] password = DigestUtils.bytesToChars(pwd); + properties.put(Context.SECURITY_CREDENTIALS, new String(password)); + } else { + properties.put(Context.SECURITY_AUTHENTICATION, "GSSAPI"); + } + return new LdapUserAdmin(properties, true); + } + +// protected InitialLdapContext getLdapContext() { +// return initialLdapContext; +// } + + @Override + protected Boolean daoHasRole(LdapName dn) { + try { + return daoGetRole(dn) != null; + } catch (NameNotFoundException e) { + return false; + } + } + + @Override + protected DirectoryUser daoGetRole(LdapName name) throws NameNotFoundException { + try { + Attributes attrs = ldapConnection.getAttributes(name); + if (attrs.size() == 0) + return null; + int roleType = roleType(name); + LdifUser res; + if (roleType == Role.GROUP) + res = new LdifGroup(this, name, attrs); + else if (roleType == Role.USER) + res = new LdifUser(this, name, attrs); + else + throw new UserDirectoryException("Unsupported LDAP type for " + name); + return res; + } catch (NameNotFoundException e) { + throw e; + } catch (NamingException e) { + return null; + } + } + + @Override + protected List doGetRoles(Filter f) { + ArrayList res = new ArrayList(); + try { + String searchFilter = f != null ? f.toString() + : "(|(" + objectClass + "=" + getUserObjectClass() + ")(" + objectClass + "=" + + getGroupObjectClass() + "))"; + SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + + LdapName searchBase = getBaseDn(); + NamingEnumeration results = ldapConnection.search(searchBase, searchFilter, searchControls); + + results: while (results.hasMoreElements()) { + SearchResult searchResult = results.next(); + Attributes attrs = searchResult.getAttributes(); + Attribute objectClassAttr = attrs.get(objectClass.name()); + LdapName dn = toDn(searchBase, searchResult); + LdifUser role; + if (objectClassAttr.contains(getGroupObjectClass()) + || objectClassAttr.contains(getGroupObjectClass().toLowerCase())) + role = new LdifGroup(this, dn, attrs); + else if (objectClassAttr.contains(getUserObjectClass()) + || objectClassAttr.contains(getUserObjectClass().toLowerCase())) + role = new LdifUser(this, dn, attrs); + else { +// log.warn("Unsupported LDAP type for " + searchResult.getName()); + continue results; + } + res.add(role); + } + return res; + } catch (AuthenticationNotSupportedException e) { + // ignore (typically an unsupported anonymous bind) + // TODO better logging + return res; + } catch (Exception e) { + e.printStackTrace(); + throw new UserDirectoryException("Cannot get roles for filter " + f, e); + } + } + + private LdapName toDn(LdapName baseDn, Binding binding) throws InvalidNameException { + return new LdapName(binding.isRelative() ? binding.getName() + "," + baseDn : binding.getName()); + } + + @Override + protected List getDirectGroups(LdapName dn) { + List directGroups = new ArrayList(); + try { + String searchFilter = "(&(" + objectClass + "=" + getGroupObjectClass() + ")(" + getMemberAttributeId() + + "=" + dn + "))"; + + SearchControls searchControls = new SearchControls(); + searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); + + LdapName searchBase = getBaseDn(); + NamingEnumeration results = ldapConnection.search(searchBase, searchFilter, searchControls); + + while (results.hasMoreElements()) { + SearchResult searchResult = (SearchResult) results.nextElement(); + directGroups.add(toDn(searchBase, searchResult)); + } + return directGroups; + } catch (Exception e) { + throw new UserDirectoryException("Cannot populate direct members of " + dn, e); + } + } + + @Override + protected void prepare(UserDirectoryWorkingCopy wc) { + try { + ldapConnection.prepareChanges(wc); + } catch (NamingException e) { + throw new UserDirectoryException("Cannot prepare LDAP", e); + } + } + + @Override + protected void commit(UserDirectoryWorkingCopy wc) { + try { + ldapConnection.commitChanges(wc); + } catch (NamingException e) { + throw new UserDirectoryException("Cannot commit LDAP", e); + } + } + + @Override + protected void rollback(UserDirectoryWorkingCopy wc) { + // prepare not impacting + } + +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifAuthorization.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifAuthorization.java new file mode 100644 index 000000000..354f8c0e2 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifAuthorization.java @@ -0,0 +1,85 @@ +package org.argeo.osgi.useradmin; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Dictionary; +import java.util.List; + +import org.argeo.naming.LdapAttrs; +import org.osgi.service.useradmin.Authorization; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.User; + +/** Basic authorization. */ +class LdifAuthorization implements Authorization { + private final String name; + private final String displayName; + private final List allRoles; + + public LdifAuthorization(User user, List allRoles) { + if (user == null) { + this.name = null; + this.displayName = "anonymous"; + } else { + this.name = user.getName(); + this.displayName = extractDisplayName(user); + } + // roles + String[] roles = new String[allRoles.size()]; + for (int i = 0; i < allRoles.size(); i++) { + roles[i] = allRoles.get(i).getName(); + } + this.allRoles = Collections.unmodifiableList(Arrays.asList(roles)); + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean hasRole(String name) { + return allRoles.contains(name); + } + + @Override + public String[] getRoles() { + return allRoles.toArray(new String[allRoles.size()]); + } + + @Override + public int hashCode() { + if (name == null) + return super.hashCode(); + return name.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Authorization)) + return false; + Authorization that = (Authorization) obj; + if (name == null) + return that.getName() == null; + return name.equals(that.getName()); + } + + @Override + public String toString() { + return displayName; + } + + final static String extractDisplayName(User user) { + Dictionary props = user.getProperties(); + Object displayName = props.get(LdapAttrs.displayName); + if (displayName == null) + displayName = props.get(LdapAttrs.cn); + if (displayName == null) + displayName = props.get(LdapAttrs.uid); + if (displayName == null) + displayName = user.getName(); + if (displayName == null) + throw new UserDirectoryException("Cannot set display name for " + user); + return displayName.toString(); + } +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifGroup.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifGroup.java new file mode 100644 index 000000000..f4e558348 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifGroup.java @@ -0,0 +1,124 @@ +package org.argeo.osgi.useradmin; + +import java.util.ArrayList; +import java.util.List; + +import javax.naming.InvalidNameException; +import javax.naming.NamingEnumeration; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; + +import org.osgi.service.useradmin.Role; + +/** Directory group implementation */ +class LdifGroup extends LdifUser implements DirectoryGroup { + private final String memberAttributeId; + + LdifGroup(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes) { + super(userAdmin, dn, attributes); + memberAttributeId = userAdmin.getMemberAttributeId(); + } + + @Override + public boolean addMember(Role role) { + try { + Role foundRole = findRole(new LdapName(role.getName())); + if (foundRole == null) + throw new UnsupportedOperationException( + "Adding role " + role.getName() + " is unsupported within this context."); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Role name" + role.getName() + " is badly formatted"); + } + + getUserAdmin().checkEdit(); + if (!isEditing()) + startEditing(); + + Attribute member = getAttributes().get(memberAttributeId); + if (member != null) { + if (member.contains(role.getName())) + return false; + else + member.add(role.getName()); + } else + getAttributes().put(memberAttributeId, role.getName()); + return true; + } + + @Override + public boolean addRequiredMember(Role role) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeMember(Role role) { + getUserAdmin().checkEdit(); + if (!isEditing()) + startEditing(); + + Attribute member = getAttributes().get(memberAttributeId); + if (member != null) { + if (!member.contains(role.getName())) + return false; + member.remove(role.getName()); + return true; + } else + return false; + } + + @Override + public Role[] getMembers() { + List directMembers = new ArrayList(); + for (LdapName ldapName : getMemberNames()) { + Role role = findRole(ldapName); + if (role == null) { + throw new UserDirectoryException("Role " + ldapName + " cannot be added."); + } + directMembers.add(role); + } + return directMembers.toArray(new Role[directMembers.size()]); + } + + /** + * Whether a role with this name can be found from this context. + * + * @return The related {@link Role} or null. + */ + protected Role findRole(LdapName ldapName) { + Role role = getUserAdmin().getRole(ldapName.toString()); + if (role == null) { + if (getUserAdmin().getExternalRoles() != null) + role = getUserAdmin().getExternalRoles().getRole(ldapName.toString()); + } + return role; + } + + @Override + public List getMemberNames() { + Attribute memberAttribute = getAttributes().get(memberAttributeId); + 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 (Exception e) { + throw new UserDirectoryException("Cannot get members", e); + } + } + + @Override + public Role[] getRequiredMembers() { + throw new UnsupportedOperationException(); + } + + @Override + public int getType() { + return GROUP; + } +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java new file mode 100644 index 000000000..b3e7f5955 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUser.java @@ -0,0 +1,409 @@ +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.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; + +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.naming.AuthPassword; +import org.argeo.naming.LdapAttrs; +import org.argeo.naming.SharedSecret; + +/** Directory user implementation */ +class LdifUser implements DirectoryUser { + private final AbstractUserDirectory userAdmin; + + private final LdapName dn; + + private final boolean frozen; + private Attributes publishedAttributes; + + private final AttributeDictionary properties; + private final AttributeDictionary credentials; + + LdifUser(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes) { + this(userAdmin, dn, attributes, false); + } + + private LdifUser(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes, boolean frozen) { + this.userAdmin = userAdmin; + this.dn = dn; + this.publishedAttributes = attributes; + properties = new AttributeDictionary(false); + credentials = new AttributeDictionary(true); + this.frozen = frozen; + } + + @Override + public String getName() { + return dn.toString(); + } + + @Override + public int getType() { + 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 = DigestUtils.bytesToChars(value); + 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(), DigestUtils.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 = DigestUtils.bytesToChars((byte[]) value); + byte[] valueBytes; + if (DigestUtils.PASSWORD_SCHEME_SHA.equals(passwordScheme)) { + valueBytes = DigestUtils.toPasswordScheme(passwordScheme, passwordValue, null, null, null); + } else if (DigestUtils.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 = DigestUtils.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(DigestUtils.sha1(DigestUtils.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; +// } + + @Override + public LdapName getDn() { + return dn; + } + + @Override + public synchronized Attributes getAttributes() { + return isEditing() ? getModifiedAttributes() : publishedAttributes; + } + + /** Should only be called from working copy thread. */ + private synchronized Attributes getModifiedAttributes() { + assert getWc() != null; + return getWc().getAttributes(getDn()); + } + + protected synchronized boolean isEditing() { + return getWc() != null && getModifiedAttributes() != null; + } + + private synchronized UserDirectoryWorkingCopy getWc() { + return userAdmin.getWorkingCopy(); + } + + protected synchronized void startEditing() { + if (frozen) + throw new UserDirectoryException("Cannot edit frozen view"); + if (getUserAdmin().isReadOnly()) + throw new UserDirectoryException("User directory is read-only"); + assert getModifiedAttributes() == null; + getWc().startEditing(this); + // modifiedAttributes = (Attributes) publishedAttributes.clone(); + } + + public synchronized void publishAttributes(Attributes modifiedAttributes) { + publishedAttributes = modifiedAttributes; + } + + public DirectoryUser getPublished() { + return new LdifUser(userAdmin, dn, publishedAttributes, true); + } + + @Override + public int hashCode() { + return dn.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj instanceof LdifUser) { + LdifUser that = (LdifUser) obj; + return this.dn.equals(that.dn); + } + return false; + } + + @Override + public String toString() { + return dn.toString(); + } + + protected AbstractUserDirectory getUserAdmin() { + return userAdmin; + } + + private class AttributeDictionary extends Dictionary { + private final List effectiveKeys = new ArrayList(); + private final List attrFilter; + private final Boolean includeFilter; + + public AttributeDictionary(Boolean includeFilter) { + this.attrFilter = userAdmin.getCredentialAttributeIds(); + this.includeFilter = includeFilter; + try { + NamingEnumeration ids = getAttributes().getIDs(); + while (ids.hasMore()) { + String id = ids.next(); + if (includeFilter && attrFilter.contains(id)) + effectiveKeys.add(id); + else if (!includeFilter && !attrFilter.contains(id)) + effectiveKeys.add(id); + } + } catch (NamingException e) { + throw new UserDirectoryException("Cannot initialise attribute dictionary", e); + } + } + + @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 = getAttributes().get(key.toString()); + 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; + if (!attr.getID().equals(LdapAttrs.objectClass.name())) + return value; + // special case for object class + NamingEnumeration en = attr.getAll(); + Set objectClasses = new HashSet(); + while (en.hasMore()) { + String objectClass = en.next().toString(); + objectClasses.add(objectClass); + } + + if (objectClasses.contains(userAdmin.getUserObjectClass())) + return userAdmin.getUserObjectClass(); + else if (objectClasses.contains(userAdmin.getGroupObjectClass())) + return userAdmin.getGroupObjectClass(); + else + return value; + } catch (NamingException e) { + throw new UserDirectoryException("Cannot get value for attribute " + key, e); + } + } + + @Override + public Object put(String key, Object value) { + if (key == null) { + // TODO persist to other sources (like PKCS12) + char[] password = DigestUtils.bytesToChars(value); + byte[] hashedPassword = sha1hash(password); + return put(LdapAttrs.userPassword.name(), hashedPassword); + } + if (key.startsWith("X-")) { + return put(LdapAttrs.authPassword.name(), value); + } + + userAdmin.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 UserDirectoryException("Cannot get value for attribute " + key, e); + } + } + + @Override + public Object remove(Object key) { + userAdmin.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 UserDirectoryException("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; + } + +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUserAdmin.java b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUserAdmin.java new file mode 100644 index 000000000..c32bbc53f --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/LdifUserAdmin.java @@ -0,0 +1,260 @@ +package org.argeo.osgi.useradmin; + +import static org.argeo.naming.LdapAttrs.objectClass; +import static org.argeo.naming.LdapObjs.inetOrgPerson; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Dictionary; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.List; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; + +import javax.naming.NameNotFoundException; +import javax.naming.NamingEnumeration; +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; + +import org.argeo.naming.LdifParser; +import org.argeo.naming.LdifWriter; +import org.osgi.framework.Filter; +import org.osgi.service.useradmin.Role; +import org.osgi.service.useradmin.User; + +/** A user admin based on a LDIF files. */ +public class LdifUserAdmin extends AbstractUserDirectory { + private SortedMap users = new TreeMap(); + private SortedMap groups = new TreeMap(); + + public LdifUserAdmin(String uri, String baseDn) { + this(fromUri(uri, baseDn), false); + } + + public LdifUserAdmin(Dictionary properties) { + this(properties, false); + } + + protected LdifUserAdmin(Dictionary properties, boolean scoped) { + super(null, properties, scoped); + } + + public LdifUserAdmin(URI uri, Dictionary properties) { + super(uri, properties, false); + } + + @Override + protected AbstractUserDirectory scope(User user) { + Dictionary credentials = user.getCredentials(); + String username = (String) credentials.get(SHARED_STATE_USERNAME); + if (username == null) + username = user.getName(); + Object pwdCred = credentials.get(SHARED_STATE_PASSWORD); + byte[] pwd = (byte[]) pwdCred; + if (pwd != null) { + char[] password = DigestUtils.bytesToChars(pwd); + User directoryUser = (User) getRole(username); + if (!directoryUser.hasCredential(null, password)) + throw new UserDirectoryException("Invalid credentials"); + } else { + throw new UserDirectoryException("Password is required"); + } + Dictionary properties = cloneProperties(); + properties.put(UserAdminConf.readOnly.name(), "true"); + LdifUserAdmin scopedUserAdmin = new LdifUserAdmin(properties, true); + scopedUserAdmin.groups = Collections.unmodifiableSortedMap(groups); + scopedUserAdmin.users = Collections.unmodifiableSortedMap(users); + return scopedUserAdmin; + } + + private static Dictionary fromUri(String uri, String baseDn) { + Hashtable res = new Hashtable(); + res.put(UserAdminConf.uri.name(), uri); + res.put(UserAdminConf.baseDn.name(), baseDn); + return res; + } + + public void init() { + + try { + URI u = new URI(getUri()); + if (u.getScheme().equals("file")) { + File file = new File(u); + if (!file.exists()) + return; + } + load(u.toURL().openStream()); + } catch (Exception e) { + throw new UserDirectoryException("Cannot open URL " + getUri(), e); + } + } + + public void save() { + if (getUri() == null) + throw new UserDirectoryException("Cannot save LDIF user admin: no URI is set"); + if (isReadOnly()) + throw new UserDirectoryException("Cannot save LDIF user admin: " + getUri() + " is read-only"); + try (FileOutputStream out = new FileOutputStream(new File(new URI(getUri())))) { + save(out); + } catch (IOException | URISyntaxException e) { + throw new UserDirectoryException("Cannot save user admin to " + getUri(), e); + } + } + + public void save(OutputStream out) throws IOException { + try { + LdifWriter ldifWriter = new LdifWriter(out); + for (LdapName name : groups.keySet()) + ldifWriter.writeEntry(name, groups.get(name).getAttributes()); + for (LdapName name : users.keySet()) + ldifWriter.writeEntry(name, users.get(name).getAttributes()); + } finally { + out.close(); + } + } + + protected void load(InputStream in) { + try { + users.clear(); + groups.clear(); + + LdifParser ldifParser = new LdifParser(); + SortedMap allEntries = ldifParser.read(in); + for (LdapName key : allEntries.keySet()) { + Attributes attributes = allEntries.get(key); + // check for inconsistency + Set lowerCase = new HashSet(); + NamingEnumeration ids = attributes.getIDs(); + while (ids.hasMoreElements()) { + String id = ids.nextElement().toLowerCase(); + if (lowerCase.contains(id)) + throw new UserDirectoryException(key + " has duplicate id " + id); + lowerCase.add(id); + } + + // analyse object classes + NamingEnumeration objectClasses = attributes.get(objectClass.name()).getAll(); + // System.out.println(key); + objectClasses: while (objectClasses.hasMore()) { + String objectClass = objectClasses.next().toString(); + // System.out.println(" " + objectClass); + if (objectClass.toLowerCase().equals(inetOrgPerson.name().toLowerCase())) { + users.put(key, new LdifUser(this, key, attributes)); + break objectClasses; + } else if (objectClass.toLowerCase().equals(getGroupObjectClass().toLowerCase())) { + groups.put(key, new LdifGroup(this, key, attributes)); + break objectClasses; + } + } + } + } catch (Exception e) { + throw new UserDirectoryException("Cannot load user admin service from LDIF", e); + } + } + + public void destroy() { + if (users == null || groups == null) + throw new UserDirectoryException("User directory " + getBaseDn() + " is already destroyed"); + users = null; + groups = null; + } + + @Override + protected DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException { + if (groups.containsKey(key)) + return groups.get(key); + if (users.containsKey(key)) + return users.get(key); + throw new NameNotFoundException(key + " not persisted"); + } + + @Override + protected Boolean daoHasRole(LdapName dn) { + return users.containsKey(dn) || groups.containsKey(dn); + } + + protected List doGetRoles(Filter f) { + ArrayList res = new ArrayList(); + if (f == null) { + res.addAll(users.values()); + res.addAll(groups.values()); + } else { + for (DirectoryUser user : users.values()) { + if (f.match(user.getProperties())) + res.add(user); + } + for (DirectoryUser group : groups.values()) + if (f.match(group.getProperties())) + res.add(group); + } + return res; + } + + @Override + protected List getDirectGroups(LdapName dn) { + List directGroups = new ArrayList(); + for (LdapName name : groups.keySet()) { + DirectoryGroup group = groups.get(name); + if (group.getMemberNames().contains(dn)) + directGroups.add(group.getDn()); + } + return directGroups; + } + + @Override + protected void prepare(UserDirectoryWorkingCopy wc) { + // delete + for (LdapName dn : wc.getDeletedUsers().keySet()) { + if (users.containsKey(dn)) + users.remove(dn); + else if (groups.containsKey(dn)) + groups.remove(dn); + else + throw new UserDirectoryException("User to delete not found " + dn); + } + // add + for (LdapName dn : wc.getNewUsers().keySet()) { + DirectoryUser user = wc.getNewUsers().get(dn); + if (users.containsKey(dn) || groups.containsKey(dn)) + throw new UserDirectoryException("User to create found " + dn); + else if (Role.USER == user.getType()) + users.put(dn, user); + else if (Role.GROUP == user.getType()) + groups.put(dn, (DirectoryGroup) user); + else + throw new UserDirectoryException("Unsupported role type " + user.getType() + " for new user " + dn); + } + // modify + for (LdapName dn : wc.getModifiedUsers().keySet()) { + Attributes modifiedAttrs = wc.getModifiedUsers().get(dn); + DirectoryUser user; + if (users.containsKey(dn)) + user = users.get(dn); + else if (groups.containsKey(dn)) + user = groups.get(dn); + else + throw new UserDirectoryException("User to modify no found " + dn); + user.publishAttributes(modifiedAttrs); + } + } + + @Override + protected void commit(UserDirectoryWorkingCopy wc) { + save(); + } + + @Override + protected void rollback(UserDirectoryWorkingCopy wc) { + init(); + } + +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java b/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java new file mode 100644 index 000000000..fe1ca7643 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserDirectory.java @@ -0,0 +1,66 @@ +package org.argeo.osgi.useradmin; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.List; + +import javax.naming.NameNotFoundException; +import javax.naming.NamingException; +import javax.naming.directory.Attributes; +import javax.naming.directory.BasicAttributes; +import javax.naming.ldap.LdapName; + +import org.argeo.naming.LdapAttrs; +import org.osgi.framework.Filter; +import org.osgi.service.useradmin.User; + +public class OsUserDirectory extends AbstractUserDirectory { + private final String osUsername = System.getProperty("user.name"); + private final LdapName osUserDn; + private final LdifUser osUser; + + public OsUserDirectory(URI uriArg, Dictionary props) { + super(uriArg, props, false); + try { + osUserDn = new LdapName(LdapAttrs.uid.name() + "=" + osUsername + "," + getUserBase() + "," + getBaseDn()); + Attributes attributes = new BasicAttributes(); + attributes.put(LdapAttrs.uid.name(), osUsername); + osUser = new LdifUser(this, osUserDn, attributes); + } catch (NamingException e) { + throw new UserDirectoryException("Cannot create system user", e); + } + } + + @Override + protected List getDirectGroups(LdapName dn) { + return new ArrayList<>(); + } + + @Override + protected Boolean daoHasRole(LdapName dn) { + return osUserDn.equals(dn); + } + + @Override + protected DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException { + if (osUserDn.equals(key)) + return osUser; + else + throw new NameNotFoundException("Not an OS role"); + } + + @Override + protected List doGetRoles(Filter f) { + List res = new ArrayList<>(); + if (f == null || f.match(osUser.getProperties())) + res.add(osUser); + return res; + } + + @Override + protected AbstractUserDirectory scope(User user) { + throw new UnsupportedOperationException(); + } + +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserUtils.java b/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserUtils.java new file mode 100644 index 000000000..ad6bf8816 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/OsUserUtils.java @@ -0,0 +1,53 @@ +package org.argeo.osgi.useradmin; + +import java.net.URISyntaxException; +import java.net.URL; +import java.security.NoSuchAlgorithmException; +import java.security.URIParameter; + +import javax.security.auth.Subject; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; + +public class OsUserUtils { + private static String LOGIN_CONTEXT_USER_NIX = "USER_NIX"; + private static String LOGIN_CONTEXT_USER_NT = "USER_NT"; + + public static String getOsUsername() { + return System.getProperty("user.name"); + } + + public static LoginContext loginAsSystemUser(Subject subject) { + try { + URL jaasConfigurationUrl = OsUserUtils.class.getClassLoader() + .getResource("org/argeo/osgi/useradmin/jaas-os.cfg"); + URIParameter uriParameter = new URIParameter(jaasConfigurationUrl.toURI()); + Configuration jaasConfiguration = Configuration.getInstance("JavaLoginConfig", uriParameter); + LoginContext lc = new LoginContext(isWindows() ? LOGIN_CONTEXT_USER_NT : LOGIN_CONTEXT_USER_NIX, subject, + null, jaasConfiguration); + lc.login(); + return lc; + } catch (URISyntaxException | NoSuchAlgorithmException | LoginException e) { + throw new RuntimeException("Cannot login as system user", e); + } + } + + public static void main(String args[]) { + Subject subject = new Subject(); + LoginContext loginContext = loginAsSystemUser(subject); + System.out.println(subject); + try { + loginContext.logout(); + } catch (LoginException e) { + // silent + } + } + + private static boolean isWindows() { + return System.getProperty("os.name").startsWith("Windows"); + } + + private OsUserUtils() { + } +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/TokenUtils.java b/org.argeo.util/src/org/argeo/osgi/useradmin/TokenUtils.java new file mode 100644 index 000000000..83c1d76f6 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/TokenUtils.java @@ -0,0 +1,87 @@ +package org.argeo.osgi.useradmin; + +import static org.argeo.naming.LdapAttrs.description; +import static org.argeo.naming.LdapAttrs.owner; + +import java.security.Principal; +import java.time.Instant; +import java.util.HashSet; +import java.util.Set; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.security.auth.Subject; + +import org.argeo.naming.NamingUtils; +import org.osgi.service.useradmin.Group; + +/** + * Canonically implements the Argeo token conventions. + */ +public class TokenUtils { + public static Set tokensUsed(Subject subject, String tokensBaseDn) { + Set res = new HashSet<>(); + for (Principal principal : subject.getPrincipals()) { + String name = principal.getName(); + if (name.endsWith(tokensBaseDn)) { + try { + LdapName ldapName = new LdapName(name); + String token = ldapName.getRdn(ldapName.size()).getValue().toString(); + res.add(token); + } catch (InvalidNameException e) { + throw new UserDirectoryException("Invalid principal " + principal, e); + } + } + } + return res; + } + + /** The user related to this token group */ + public static String userDn(Group tokenGroup) { + return (String) tokenGroup.getProperties().get(owner.name()); + } + + public static boolean isExpired(Group tokenGroup) { + return isExpired(tokenGroup, Instant.now()); + + } + + public static boolean isExpired(Group tokenGroup, Instant instant) { + String expiryDateStr = (String) tokenGroup.getProperties().get(description.name()); + if (expiryDateStr != null) { + Instant expiryDate = NamingUtils.ldapDateToInstant(expiryDateStr); + if (expiryDate.isBefore(instant)) { + return true; + } + } + return false; + } + +// private final String token; +// +// public TokenUtils(String token) { +// this.token = token; +// } +// +// public String getToken() { +// return token; +// } +// +// @Override +// public int hashCode() { +// return token.hashCode(); +// } +// +// @Override +// public boolean equals(Object obj) { +// if ((obj instanceof TokenUtils) && ((TokenUtils) obj).token.equals(token)) +// return true; +// return false; +// } +// +// @Override +// public String toString() { +// return "Token #" + hashCode(); +// } + +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/UserAdminConf.java b/org.argeo.util/src/org/argeo/osgi/useradmin/UserAdminConf.java new file mode 100644 index 000000000..ec41978dc --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/UserAdminConf.java @@ -0,0 +1,239 @@ +package org.argeo.osgi.useradmin; + +import java.net.InetAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.util.Dictionary; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; + +import javax.naming.Context; +import javax.naming.ldap.LdapName; + +import org.argeo.naming.NamingUtils; + +/** Properties used to configure user admins. */ +public enum UserAdminConf { + /** Base DN (cannot be configured externally) */ + baseDn("dc=example,dc=com"), + + /** URI of the underlying resource (cannot be configured externally) */ + uri("ldap://localhost:10389"), + + /** User objectClass */ + userObjectClass("inetOrgPerson"), + + /** Relative base DN for users */ + userBase("ou=People"), + + /** Groups objectClass */ + groupObjectClass("groupOfNames"), + + /** Relative base DN for users */ + groupBase("ou=Groups"), + + /** Read-only source */ + readOnly(null), + + /** Disabled source */ + disabled(null), + + /** Authentication realm */ + realm(null); + + public final static String FACTORY_PID = "org.argeo.osgi.useradmin.config"; + + public final static String SCHEME_LDAP = "ldap"; + public final static String SCHEME_LDAPS = "ldaps"; + public final static String SCHEME_FILE = "file"; + public final static String SCHEME_OS = "os"; + public final static String SCHEME_IPA = "ipa"; + + /** The default value. */ + private Object def; + + UserAdminConf(Object def) { + this.def = def; + } + + public Object getDefault() { + return def; + } + + /** + * For use as Java property. + * + * @deprecated use {@link #name()} instead + */ + @Deprecated + public String property() { + return name(); + } + + public String getValue(Dictionary properties) { + Object res = getRawValue(properties); + if (res == null) + return null; + return res.toString(); + } + + @SuppressWarnings("unchecked") + public T getRawValue(Dictionary properties) { + Object res = properties.get(name()); + if (res == null) + res = getDefault(); + return (T) res; + } + + /** @deprecated use {@link #valueOf(String)} instead */ + @Deprecated + public static UserAdminConf local(String property) { + return UserAdminConf.valueOf(property); + } + + /** Hides host and credentials. */ + public static URI propertiesAsUri(Dictionary properties) { + StringBuilder query = new StringBuilder(); + + boolean first = true; +// for (Enumeration keys = properties.keys(); keys.hasMoreElements();) { +// String key = keys.nextElement(); +// // TODO clarify which keys are relevant (list only the enum?) +// if (!key.equals("service.factoryPid") && !key.equals("cn") && !key.equals("dn") +// && !key.equals(Constants.SERVICE_PID) && !key.startsWith("java") && !key.equals(baseDn.name()) +// && !key.equals(uri.name()) && !key.equals(Constants.OBJECTCLASS) +// && !key.equals(Constants.SERVICE_ID) && !key.equals("bundle.id")) { +// if (first) +// first = false; +// else +// query.append('&'); +// query.append(valueOf(key).name()); +// query.append('=').append(properties.get(key).toString()); +// } +// } + + keys: for (UserAdminConf key : UserAdminConf.values()) { + if (key.equals(baseDn) || key.equals(uri)) + continue keys; + Object value = properties.get(key.name()); + if (value == null) + continue keys; + if (first) + first = false; + else + query.append('&'); + query.append(key.name()); + query.append('=').append(value.toString()); + + } + + Object bDnObj = properties.get(baseDn.name()); + String bDn = bDnObj != null ? bDnObj.toString() : null; + try { + return new URI(null, null, bDn != null ? '/' + bDn : null, query.length() != 0 ? query.toString() : null, + null); + } catch (URISyntaxException e) { + throw new UserDirectoryException("Cannot create URI from properties", e); + } + } + + public static Dictionary uriAsProperties(String uriStr) { + try { + Hashtable res = new Hashtable(); + URI u = new URI(uriStr); + String scheme = u.getScheme(); + if (scheme != null && scheme.equals(SCHEME_IPA)) { + return IpaUtils.convertIpaUri(u); +// scheme = u.getScheme(); + } + String path = u.getPath(); + // base DN + String bDn = path.substring(path.lastIndexOf('/') + 1, path.length()); + if (bDn.equals("") && SCHEME_OS.equals(scheme)) { + bDn = getBaseDnFromHostname(); + } + + if (bDn.endsWith(".ldif")) + bDn = bDn.substring(0, bDn.length() - ".ldif".length()); + + // Normalize base DN as LDAP name + bDn = new LdapName(bDn).toString(); + + String principal = null; + String credentials = null; + if (scheme != null) + if (scheme.equals(SCHEME_LDAP) || scheme.equals(SCHEME_LDAPS)) { + // TODO additional checks + if (u.getUserInfo() != null) { + String[] userInfo = u.getUserInfo().split(":"); + principal = userInfo.length > 0 ? userInfo[0] : null; + credentials = userInfo.length > 1 ? userInfo[1] : null; + } + } else if (scheme.equals(SCHEME_FILE)) { + } else if (scheme.equals(SCHEME_IPA)) { + } else if (scheme.equals(SCHEME_OS)) { + } else + throw new UserDirectoryException("Unsupported scheme " + scheme); + Map> query = NamingUtils.queryToMap(u); + for (String key : query.keySet()) { + UserAdminConf ldapProp = UserAdminConf.valueOf(key); + List values = query.get(key); + if (values.size() == 1) { + res.put(ldapProp.name(), values.get(0)); + } else { + throw new UserDirectoryException("Only single values are supported"); + } + } + res.put(baseDn.name(), bDn); + if (SCHEME_OS.equals(scheme)) + res.put(readOnly.name(), "true"); + if (principal != null) + res.put(Context.SECURITY_PRINCIPAL, principal); + if (credentials != null) + res.put(Context.SECURITY_CREDENTIALS, credentials); + if (scheme != null) {// relative URIs are dealt with externally + if (SCHEME_OS.equals(scheme)) { + res.put(uri.name(), SCHEME_OS + ":///"); + } else { + URI bareUri = new URI(scheme, null, u.getHost(), u.getPort(), + scheme.equals(SCHEME_FILE) ? u.getPath() : null, null, null); + res.put(uri.name(), bareUri.toString()); + } + } + return res; + } catch (Exception e) { + throw new UserDirectoryException("Cannot convert " + uri + " to properties", e); + } + } + + private static String getBaseDnFromHostname() { + String hostname; + try { + hostname = InetAddress.getLocalHost().getHostName(); + } catch (UnknownHostException e) { + hostname = "localhost.localdomain"; + } + int dotIdx = hostname.indexOf('.'); + if (dotIdx >= 0) { + String domain = hostname.substring(dotIdx + 1, hostname.length()); + String bDn = ("." + domain).replaceAll("\\.", ",dc="); + bDn = bDn.substring(1, bDn.length()); + return bDn; + } else { + return "dc=" + hostname; + } + } + + /** + * Hash the base DN in order to have a deterministic string to be used as a cn + * for the underlying user directory. + */ + public static String baseDnHash(Dictionary properties) { + String bDn = (String) properties.get(baseDn.name()); + if (bDn == null) + throw new UserDirectoryException("No baseDn in " + properties); + return DigestUtils.sha1str(bDn); + } +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectory.java b/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectory.java new file mode 100644 index 000000000..ff80c5ac8 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectory.java @@ -0,0 +1,25 @@ +package org.argeo.osgi.useradmin; + +import javax.naming.ldap.LdapName; +import javax.transaction.xa.XAResource; + +/** Information about a user directory. */ +public interface UserDirectory { + /** The base DN of all entries in this user directory */ + LdapName getBaseDn(); + + /** The related {@link XAResource} */ + XAResource getXaResource(); + + boolean isReadOnly(); + + boolean isDisabled(); + + String getUserObjectClass(); + + String getUserBase(); + + String getGroupObjectClass(); + + String getGroupBase(); +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectoryException.java b/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectoryException.java new file mode 100644 index 000000000..613d0fdf0 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectoryException.java @@ -0,0 +1,19 @@ +package org.argeo.osgi.useradmin; + +import org.osgi.service.useradmin.UserAdmin; + +/** + * Exceptions related to Argeo's implementation of OSGi {@link UserAdmin} + * service. + */ +public class UserDirectoryException extends RuntimeException { + private static final long serialVersionUID = 1419352360062048603L; + + public UserDirectoryException(String message) { + super(message); + } + + public UserDirectoryException(String message, Throwable e) { + super(message, e); + } +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectoryWorkingCopy.java b/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectoryWorkingCopy.java new file mode 100644 index 000000000..0e25bdfa1 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/UserDirectoryWorkingCopy.java @@ -0,0 +1,58 @@ +package org.argeo.osgi.useradmin; + +import java.util.HashMap; +import java.util.Map; + +import javax.naming.directory.Attributes; +import javax.naming.ldap.LdapName; +import javax.transaction.xa.XAResource; + +/** {@link XAResource} for a user directory being edited. */ +class UserDirectoryWorkingCopy { + // private final static Log log = LogFactory + // .getLog(UserDirectoryWorkingCopy.class); + + private Map newUsers = new HashMap(); + private Map modifiedUsers = new HashMap(); + private Map deletedUsers = new HashMap(); + + void cleanUp() { + // clean collections + newUsers.clear(); + newUsers = null; + modifiedUsers.clear(); + modifiedUsers = null; + deletedUsers.clear(); + deletedUsers = null; + } + + public boolean noModifications() { + return newUsers.size() == 0 && modifiedUsers.size() == 0 + && deletedUsers.size() == 0; + } + + public Attributes getAttributes(LdapName dn) { + if (modifiedUsers.containsKey(dn)) + return modifiedUsers.get(dn); + return null; + } + + public void startEditing(DirectoryUser user) { + LdapName dn = user.getDn(); + if (modifiedUsers.containsKey(dn)) + throw new UserDirectoryException("Already editing " + dn); + modifiedUsers.put(dn, (Attributes) user.getAttributes().clone()); + } + + public Map getNewUsers() { + return newUsers; + } + + public Map getDeletedUsers() { + return deletedUsers; + } + + public Map getModifiedUsers() { + return modifiedUsers; + } +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/WcXaResource.java b/org.argeo.util/src/org/argeo/osgi/useradmin/WcXaResource.java new file mode 100644 index 000000000..1630b6bd3 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/WcXaResource.java @@ -0,0 +1,135 @@ +package org.argeo.osgi.useradmin; + +import java.util.HashMap; +import java.util.Map; + +import javax.transaction.xa.XAException; +import javax.transaction.xa.XAResource; +import javax.transaction.xa.Xid; + +/** {@link XAResource} for a user directory being edited. */ +class WcXaResource implements XAResource { + private final AbstractUserDirectory userDirectory; + + private Map workingCopies = new HashMap(); + private Xid editingXid = null; + private int transactionTimeout = 0; + + public WcXaResource(AbstractUserDirectory userDirectory) { + this.userDirectory = userDirectory; + } + + @Override + public synchronized void start(Xid xid, int flags) throws XAException { + if (editingXid != null) + throw new UserDirectoryException("Already editing " + editingXid); + UserDirectoryWorkingCopy wc = workingCopies.put(xid, new UserDirectoryWorkingCopy()); + if (wc != null) + throw new UserDirectoryException("There is already a working copy for " + xid); + this.editingXid = xid; + } + + @Override + public void end(Xid xid, int flags) throws XAException { + checkXid(xid); + } + + private UserDirectoryWorkingCopy wc(Xid xid) { + return workingCopies.get(xid); + } + + synchronized UserDirectoryWorkingCopy wc() { + if (editingXid == null) + return null; + UserDirectoryWorkingCopy wc = workingCopies.get(editingXid); + if (wc == null) + throw new UserDirectoryException("No working copy found for " + editingXid); + return wc; + } + + private synchronized void cleanUp(Xid xid) { + wc(xid).cleanUp(); + workingCopies.remove(xid); + editingXid = null; + } + + @Override + public int prepare(Xid xid) throws XAException { + checkXid(xid); + UserDirectoryWorkingCopy wc = wc(xid); + if (wc.noModifications()) + return XA_RDONLY; + try { + userDirectory.prepare(wc); + } catch (Exception e) { + e.printStackTrace(); + throw new XAException(XAException.XAER_RMERR); + } + return XA_OK; + } + + @Override + public void commit(Xid xid, boolean onePhase) throws XAException { + try { + checkXid(xid); + UserDirectoryWorkingCopy wc = wc(xid); + if (wc.noModifications()) + return; + if (onePhase) + userDirectory.prepare(wc); + userDirectory.commit(wc); + } catch (Exception e) { + e.printStackTrace(); + throw new XAException(XAException.XAER_RMERR); + } finally { + cleanUp(xid); + } + } + + @Override + public void rollback(Xid xid) throws XAException { + try { + checkXid(xid); + userDirectory.rollback(wc(xid)); + } catch (Exception e) { + e.printStackTrace(); + throw new XAException(XAException.XAER_RMERR); + } finally { + cleanUp(xid); + } + } + + @Override + public void forget(Xid xid) throws XAException { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isSameRM(XAResource xares) throws XAException { + return xares == this; + } + + @Override + public Xid[] recover(int flag) throws XAException { + return new Xid[0]; + } + + @Override + public int getTransactionTimeout() throws XAException { + return transactionTimeout; + } + + @Override + public boolean setTransactionTimeout(int seconds) throws XAException { + transactionTimeout = seconds; + return true; + } + + private void checkXid(Xid xid) throws XAException { + if (xid == null) + throw new XAException(XAException.XAER_OUTSIDE); + if (!xid.equals(xid)) + throw new XAException(XAException.XAER_NOTA); + } + +} diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/jaas-os.cfg b/org.argeo.util/src/org/argeo/osgi/useradmin/jaas-os.cfg new file mode 100644 index 000000000..da04505a7 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/jaas-os.cfg @@ -0,0 +1,8 @@ +USER_NIX { + com.sun.security.auth.module.UnixLoginModule requisite; +}; + +USER_NT { + com.sun.security.auth.module.NTLoginModule requisite; +}; + diff --git a/org.argeo.util/src/org/argeo/osgi/useradmin/package-info.java b/org.argeo.util/src/org/argeo/osgi/useradmin/package-info.java new file mode 100644 index 000000000..c108d2c55 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/useradmin/package-info.java @@ -0,0 +1,2 @@ +/** LDAP and LDIF based OSGi useradmin implementation. */ +package org.argeo.osgi.useradmin; \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/osgi/util/FilterRequirement.java b/org.argeo.util/src/org/argeo/osgi/util/FilterRequirement.java new file mode 100644 index 000000000..31f1d4de6 --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/util/FilterRequirement.java @@ -0,0 +1,42 @@ +package org.argeo.osgi.util; + +import java.util.HashMap; +import java.util.Map; + +import org.osgi.resource.Namespace; +import org.osgi.resource.Requirement; +import org.osgi.resource.Resource; + +/** Simplify filtering resources. */ +public class FilterRequirement implements Requirement { + private String namespace; + private String filter; + + public FilterRequirement(String namespace, String filter) { + this.namespace = namespace; + this.filter = filter; + } + + @Override + public Resource getResource() { + return null; + } + + @Override + public String getNamespace() { + return namespace; + } + + @Override + public Map getDirectives() { + Map directives = new HashMap<>(); + directives.put(Namespace.REQUIREMENT_FILTER_DIRECTIVE, filter); + return directives; + } + + @Override + public Map getAttributes() { + return new HashMap<>(); + } + +} diff --git a/org.argeo.util/src/org/argeo/osgi/util/OnServiceRegistration.java b/org.argeo.util/src/org/argeo/osgi/util/OnServiceRegistration.java new file mode 100644 index 000000000..5a6760e0f --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/util/OnServiceRegistration.java @@ -0,0 +1,98 @@ +package org.argeo.osgi.util; + +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Function; + +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceReference; +import org.osgi.util.tracker.ServiceTracker; + +public class OnServiceRegistration implements Future { + private BundleContext ownBundleContext = FrameworkUtil.getBundle(OnServiceRegistration.class).getBundleContext(); + + private ServiceTracker st; + + private R result; + private boolean cancelled = false; + private Throwable exception; + + public OnServiceRegistration(Class clss, Function function) { + this(null, clss, function); + } + + public OnServiceRegistration(BundleContext bundleContext, Class clss, Function function) { + st = new ServiceTracker(bundleContext != null ? bundleContext : ownBundleContext, clss, null) { + + @Override + public T addingService(ServiceReference reference) { + T service = super.addingService(reference); + try { + if (result != null)// we only want the first one + return service; + result = function.apply(service); + return service; + } catch (Exception e) { + exception = e; + return service; + } finally { + close(); + } + } + }; + st.open(bundleContext == null); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + if (result != null || exception != null || cancelled) + return false; + st.close(); + cancelled = true; + return true; + } + + @Override + public boolean isCancelled() { + return cancelled; + } + + @Override + public boolean isDone() { + return result != null || cancelled; + } + + @Override + public R get() throws InterruptedException, ExecutionException { + st.waitForService(0); + return tryGetResult(); + } + + @Override + public R get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + st.waitForService(TimeUnit.MILLISECONDS.convert(timeout, unit)); + if (result == null) + throw new TimeoutException("No result after " + timeout + " " + unit); + return tryGetResult(); + } + + protected R tryGetResult() throws ExecutionException, CancellationException { + if (cancelled) + throw new CancellationException(); + if (exception != null) + throw new ExecutionException(exception); + if (result == null)// this should not happen + try { + throw new IllegalStateException("No result available"); + } catch (Exception e) { + exception = e; + throw new ExecutionException(e); + } + return result; + } + +} diff --git a/org.argeo.util/src/org/argeo/osgi/util/OsgiRegister.java b/org.argeo.util/src/org/argeo/osgi/util/OsgiRegister.java new file mode 100644 index 000000000..7132b7c3f --- /dev/null +++ b/org.argeo.util/src/org/argeo/osgi/util/OsgiRegister.java @@ -0,0 +1,60 @@ +package org.argeo.osgi.util; + +import java.util.ArrayList; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ForkJoinPool; + +import org.argeo.util.register.Register; +import org.argeo.util.register.Singleton; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; + +public class OsgiRegister implements Register { + private final BundleContext bundleContext; + private Executor executor; + + private CompletableFuture shutdownStarting = new CompletableFuture(); + + public OsgiRegister(BundleContext bundleContext) { + this.bundleContext = bundleContext; + // TODO experiment with dedicated executors + this.executor = ForkJoinPool.commonPool(); + } + + @Override + public Singleton set(T obj, Class clss, Map attributes, Class... classes) { + CompletableFuture> srf = new CompletableFuture>(); + CompletableFuture postRegistration = CompletableFuture.supplyAsync(() -> { + List lst = new ArrayList<>(); + lst.add(clss.getName()); + for (Class c : classes) { + lst.add(c.getName()); + } + ServiceRegistration sr = bundleContext.registerService(lst.toArray(new String[lst.size()]), obj, + new Hashtable(attributes)); + srf.complete(sr); + return obj; + }, executor); + Singleton singleton = new Singleton(clss, postRegistration); + + shutdownStarting. // + thenCompose(singleton::prepareUnregistration). // + thenRunAsync(() -> { + try { + srf.get().unregister(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + }, executor); + return singleton; + } + + public void shutdown() { + shutdownStarting.complete(null); + } +} diff --git a/org.argeo.util/src/org/argeo/util/CsvParser.java b/org.argeo.util/src/org/argeo/util/CsvParser.java new file mode 100644 index 000000000..b903f7722 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/CsvParser.java @@ -0,0 +1,242 @@ +package org.argeo.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Parses a CSV file interpreting the first line as a header. The + * {@link #parse(InputStream)} method and the setters are synchronized so that + * the object cannot be modified when parsing. + */ +public abstract class CsvParser { + private char separator = ','; + private char quote = '\"'; + + private Boolean noHeader = false; + private Boolean strictLineAsLongAsHeader = true; + + /** + * Actually process a parsed line. If + * {@link #setStrictLineAsLongAsHeader(Boolean)} is true (default) the header + * and the tokens are guaranteed to have the same size. + * + * @param lineNumber the current line number, starts at 1 (the header, if header + * processing is enabled, the first line otherwise) + * @param header the read-only header or null if + * {@link #setNoHeader(Boolean)} is true (default is false) + * @param tokens the parsed tokens + */ + protected abstract void processLine(Integer lineNumber, List header, List tokens); + + /** + * Parses the CSV file (stream is closed at the end) + * + * @param in the stream to parse + * + * @deprecated Use {@link #parse(InputStream, Charset)} instead. + */ + @Deprecated + public synchronized void parse(InputStream in) { + parse(in, (Charset) null); + } + + /** + * Parses the CSV file (stream is closed at the end) + * + * @param in the stream to parse + * @param encoding the encoding to use. + * + * @deprecated Use {@link #parse(InputStream, Charset)} instead. + */ + @Deprecated + public synchronized void parse(InputStream in, String encoding) { + Reader reader; + if (encoding == null) + reader = new InputStreamReader(in); + else + try { + reader = new InputStreamReader(in, encoding); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException(e); + } + parse(reader); + } + + /** + * Parses the CSV file (stream is closed at the end) + * + * @param in the stream to parse + * @param charset the charset to use + */ + public synchronized void parse(InputStream in, Charset charset) { + Reader reader; + if (charset == null) + reader = new InputStreamReader(in); + else + reader = new InputStreamReader(in, charset); + parse(reader); + } + + /** + * Parses the CSV file (stream is closed at the end) + * + * @param reader the reader to use (it will be buffered) + */ + public synchronized void parse(Reader reader) { + Integer lineCount = 0; + try (BufferedReader bufferedReader = new BufferedReader(reader)) { + List header = null; + if (!noHeader) { + String headerStr = bufferedReader.readLine(); + if (headerStr == null)// empty file + return; + lineCount++; + header = new ArrayList(); + StringBuffer currStr = new StringBuffer(""); + Boolean wasInquote = false; + while (parseLine(headerStr, header, currStr, wasInquote)) { + headerStr = bufferedReader.readLine(); + if (headerStr == null) + break; + wasInquote = true; + } + header = Collections.unmodifiableList(header); + } + + String line = null; + lines: while ((line = bufferedReader.readLine()) != null) { + line = preProcessLine(line); + if (line == null) { + // skip line + continue lines; + } + lineCount++; + List tokens = new ArrayList(); + StringBuffer currStr = new StringBuffer(""); + Boolean wasInquote = false; + sublines: while (parseLine(line, tokens, currStr, wasInquote)) { + line = bufferedReader.readLine(); + if (line == null) + break sublines; + wasInquote = true; + } + if (!noHeader && strictLineAsLongAsHeader) { + int headerSize = header.size(); + int tokenSize = tokens.size(); + if (tokenSize == 1 && line.trim().equals("")) + continue lines;// empty line + if (headerSize != tokenSize) { + throw new IllegalStateException("Token size " + tokenSize + " is different from header size " + + headerSize + " at line " + lineCount + ", line: " + line + ", header: " + header + + ", tokens: " + tokens); + } + } + processLine(lineCount, header, tokens); + } + } catch (IOException e) { + throw new RuntimeException("Cannot parse CSV file (line: " + lineCount + ")", e); + } + } + + /** + * Called before each (logical) line is processed, giving a change to modify it + * (typically for cleaning dirty files). To be overridden, return the line + * unchanged by default. Skip the line if 'null' is returned. + */ + protected String preProcessLine(String line) { + return line; + } + + /** + * Parses a line character by character for performance purpose + * + * @return whether to continue parsing this line + */ + protected Boolean parseLine(String str, List tokens, StringBuffer currStr, Boolean wasInquote) { + if (wasInquote) + currStr.append('\n'); + + char[] arr = str.toCharArray(); + boolean inQuote = wasInquote; + for (int i = 0; i < arr.length; i++) { + char c = arr[i]; + if (c == separator) { + if (!inQuote) { + tokens.add(currStr.toString()); +// currStr.delete(0, currStr.length()); + currStr.setLength(0); + currStr.trimToSize(); + } else { + // we don't remove separator that are in a quoted substring + // System.out + // .println("IN QUOTE, got a separator: [" + c + "]"); + currStr.append(c); + } + } else if (c == quote) { + if (inQuote && (i + 1) < arr.length && arr[i + 1] == quote) { + // case of double quote + currStr.append(quote); + i++; + } else {// standard + inQuote = inQuote ? false : true; + } + } else { + currStr.append(c); + } + } + + if (!inQuote) { + tokens.add(currStr.toString()); + // System.out.println("# TOKEN: " + currStr); + } + // if (inQuote) + // throw new ArgeoException("Missing quote at the end of the line " + // + str + " (parsed: " + tokens + ")"); + if (inQuote) + return true; + else + return false; + // return tokens; + } + + public char getSeparator() { + return separator; + } + + public synchronized void setSeparator(char separator) { + this.separator = separator; + } + + public char getQuote() { + return quote; + } + + public synchronized void setQuote(char quote) { + this.quote = quote; + } + + public Boolean getNoHeader() { + return noHeader; + } + + public synchronized void setNoHeader(Boolean noHeader) { + this.noHeader = noHeader; + } + + public Boolean getStrictLineAsLongAsHeader() { + return strictLineAsLongAsHeader; + } + + public synchronized void setStrictLineAsLongAsHeader(Boolean strictLineAsLongAsHeader) { + this.strictLineAsLongAsHeader = strictLineAsLongAsHeader; + } + +} diff --git a/org.argeo.util/src/org/argeo/util/CsvParserWithLinesAsMap.java b/org.argeo.util/src/org/argeo/util/CsvParserWithLinesAsMap.java new file mode 100644 index 000000000..8eb6e9463 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/CsvParserWithLinesAsMap.java @@ -0,0 +1,36 @@ +package org.argeo.util; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * CSV parser allowing to process lines as maps whose keys are the header + * fields. + */ +public abstract class CsvParserWithLinesAsMap extends CsvParser { + + /** + * Actually processes a line. + * + * @param lineNumber the current line number, starts at 1 (the header, if header + * processing is enabled, the first lien otherwise) + * @param line the parsed tokens as a map whose keys are the header fields + */ + protected abstract void processLine(Integer lineNumber, Map line); + + protected final void processLine(Integer lineNumber, List header, List tokens) { + if (header == null) + throw new IllegalArgumentException("Only CSV with header is supported"); + Map line = new HashMap(); + for (int i = 0; i < header.size(); i++) { + String key = header.get(i); + String value = null; + if (i < tokens.size()) + value = tokens.get(i); + line.put(key, value); + } + processLine(lineNumber, line); + } + +} diff --git a/org.argeo.util/src/org/argeo/util/CsvWriter.java b/org.argeo.util/src/org/argeo/util/CsvWriter.java new file mode 100644 index 000000000..c3b3a3ad7 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/CsvWriter.java @@ -0,0 +1,156 @@ +package org.argeo.util; + +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.nio.charset.Charset; +import java.util.Iterator; +import java.util.List; + +/** Write in CSV format. */ +public class CsvWriter { + private final Writer out; + + private char separator = ','; + private char quote = '\"'; + + /** + * Creates a CSV writer. + * + * @param out the stream to write to. Caller is responsible for closing it. + * + * @deprecated Use {@link #CsvWriter(OutputStream, Charset)} instead. + * + */ + @Deprecated + public CsvWriter(OutputStream out) { + this.out = new OutputStreamWriter(out); + } + + /** + * Creates a CSV writer. + * + * @param out the stream to write to. Caller is responsible for closing it. + * @param encoding the encoding to use. + * + * @deprecated Use {@link #CsvWriter(OutputStream, Charset)} instead. + */ + @Deprecated + public CsvWriter(OutputStream out, String encoding) { + try { + this.out = new OutputStreamWriter(out, encoding); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Creates a CSV writer. + * + * @param out the stream to write to. Caller is responsible for closing it. + * @param charset the charset to use + */ + public CsvWriter(OutputStream out, Charset charset) { + this.out = new OutputStreamWriter(out, charset); + } + + /** + * Creates a CSV writer. + * + * @param out the stream to write to. Caller is responsible for closing it. + */ + public CsvWriter(Writer writer) { + this.out = writer; + } + + /** + * Write a CSV line. Also used to write a header if needed (this is transparent + * for the CSV writer): simply call it first, before writing the lines. + */ + public void writeLine(List tokens) { + try { + Iterator it = tokens.iterator(); + while (it.hasNext()) { + Object obj = it.next(); + writeToken(obj != null ? obj.toString() : null); + if (it.hasNext()) + out.write(separator); + } + out.write('\n'); + out.flush(); + } catch (IOException e) { + throw new RuntimeException("Could not write " + tokens, e); + } + } + + /** + * Write a CSV line. Also used to write a header if needed (this is transparent + * for the CSV writer): simply call it first, before writing the lines. + */ + public void writeLine(Object[] tokens) { + try { + for (int i = 0; i < tokens.length; i++) { + if (tokens[i] == null) { + writeToken(null); + } else { + writeToken(tokens[i].toString()); + } + if (i != (tokens.length - 1)) + out.write(separator); + } + out.write('\n'); + out.flush(); + } catch (IOException e) { + throw new RuntimeException("Could not write " + tokens, e); + } + } + + protected void writeToken(String token) throws IOException { + if (token == null) { + // TODO configure how to deal with null + out.write(""); + return; + } + // +2 for possible quotes, another +2 assuming there would be an already + // quoted string where quotes needs to be duplicated + // another +2 for safety + // we don't want to increase buffer size while writing + StringBuffer buf = new StringBuffer(token.length() + 6); + char[] arr = token.toCharArray(); + boolean shouldQuote = false; + for (char c : arr) { + if (!shouldQuote) { + if (c == separator) + shouldQuote = true; + if (c == '\n') + shouldQuote = true; + } + + if (c == quote) { + shouldQuote = true; + // duplicate quote + buf.append(quote); + } + + // generic case + buf.append(c); + } + + if (shouldQuote == true) + out.write(quote); + out.write(buf.toString()); + if (shouldQuote == true) + out.write(quote); + } + + public void setSeparator(char separator) { + this.separator = separator; + } + + public void setQuote(char quote) { + this.quote = quote; + } + +} diff --git a/org.argeo.util/src/org/argeo/util/DictionaryKeys.java b/org.argeo.util/src/org/argeo/util/DictionaryKeys.java new file mode 100644 index 000000000..d17c86f96 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/DictionaryKeys.java @@ -0,0 +1,42 @@ +package org.argeo.util; + +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.Iterator; + +/** + * Access the keys of a {@link String}-keyed {@link Dictionary} (common throughout + * the OSGi APIs) as an {@link Iterable} so that they are easily usable in + * for-each loops. + */ +class DictionaryKeys implements Iterable { + private final Dictionary dictionary; + + public DictionaryKeys(Dictionary dictionary) { + this.dictionary = dictionary; + } + + @Override + public Iterator iterator() { + return new KeyIterator(dictionary.keys()); + } + + private static class KeyIterator implements Iterator { + private final Enumeration keys; + + KeyIterator(Enumeration keys) { + this.keys = keys; + } + + @Override + public boolean hasNext() { + return keys.hasMoreElements(); + } + + @Override + public String next() { + return keys.nextElement(); + } + + } +} diff --git a/org.argeo.util/src/org/argeo/util/DigestUtils.java b/org.argeo.util/src/org/argeo/util/DigestUtils.java new file mode 100644 index 000000000..ce018007c --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/DigestUtils.java @@ -0,0 +1,201 @@ +package org.argeo.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileChannel.MapMode; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** Utilities around cryptographic digests */ +public class DigestUtils { + public final static String MD5 = "MD5"; + public final static String SHA1 = "SHA1"; + public final static String SHA256 = "SHA-256"; + public final static String SHA512 = "SHA-512"; + + private static Boolean debug = false; + // TODO: make it configurable + private final static Integer byteBufferCapacity = 100 * 1024;// 100 KB + + public static byte[] sha1(byte[] bytes) { + try { + MessageDigest digest = MessageDigest.getInstance(SHA1); + digest.update(bytes); + byte[] checksum = digest.digest(); + return checksum; + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException(e); + } + } + + public static String digest(String algorithm, byte[] bytes) { + try { + MessageDigest digest = MessageDigest.getInstance(algorithm); + digest.update(bytes); + byte[] checksum = digest.digest(); + String res = encodeHexString(checksum); + return res; + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e); + } + } + + public static String digest(String algorithm, InputStream in) { + try { + MessageDigest digest = MessageDigest.getInstance(algorithm); + // ReadableByteChannel channel = Channels.newChannel(in); + // ByteBuffer bb = ByteBuffer.allocateDirect(byteBufferCapacity); + // while (channel.read(bb) > 0) + // digest.update(bb); + byte[] buffer = new byte[byteBufferCapacity]; + int read = 0; + while ((read = in.read(buffer)) > 0) { + digest.update(buffer, 0, read); + } + + byte[] checksum = digest.digest(); + String res = encodeHexString(checksum); + return res; + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + StreamUtils.closeQuietly(in); + } + } + + public static String digest(String algorithm, File file) { + FileInputStream fis = null; + FileChannel fc = null; + try { + fis = new FileInputStream(file); + fc = fis.getChannel(); + + // Get the file's size and then map it into memory + int sz = (int) fc.size(); + ByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, sz); + return digest(algorithm, bb); + } catch (IOException e) { + throw new IllegalArgumentException("Cannot digest " + file + " with algorithm " + algorithm, e); + } finally { + StreamUtils.closeQuietly(fis); + if (fc.isOpen()) + try { + fc.close(); + } catch (IOException e) { + // silent + } + } + } + + protected static String digest(String algorithm, ByteBuffer bb) { + long begin = System.currentTimeMillis(); + try { + MessageDigest digest = MessageDigest.getInstance(algorithm); + digest.update(bb); + byte[] checksum = digest.digest(); + String res = encodeHexString(checksum); + long end = System.currentTimeMillis(); + if (debug) + System.out.println((end - begin) + " ms / " + ((end - begin) / 1000) + " s"); + return res; + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Cannot digest with algorithm " + algorithm, e); + } + } + + public static String sha1hex(Path path) { + return digest(SHA1, path, byteBufferCapacity); + } + + public static String digest(String algorithm, Path path, long bufferSize) { + byte[] digest = digestRaw(algorithm, path, bufferSize); + return encodeHexString(digest); + } + + public static byte[] digestRaw(String algorithm, Path file, long bufferSize) { + long begin = System.currentTimeMillis(); + try { + MessageDigest md = MessageDigest.getInstance(algorithm); + FileChannel fc = FileChannel.open(file); + long fileSize = Files.size(file); + if (fileSize <= bufferSize) { + ByteBuffer bb = fc.map(MapMode.READ_ONLY, 0, fileSize); + md.update(bb); + } else { + long lastCycle = (fileSize / bufferSize) - 1; + long position = 0; + for (int i = 0; i <= lastCycle; i++) { + ByteBuffer bb; + if (i != lastCycle) { + bb = fc.map(MapMode.READ_ONLY, position, bufferSize); + position = position + bufferSize; + } else { + bb = fc.map(MapMode.READ_ONLY, position, fileSize - position); + position = fileSize; + } + md.update(bb); + } + } + long end = System.currentTimeMillis(); + if (debug) + System.out.println((end - begin) + " ms / " + ((end - begin) / 1000) + " s"); + return md.digest(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Cannot digest " + file + " with algorithm " + algorithm, e); + } catch (IOException e) { + throw new RuntimeException("Cannot digest " + file + " with algorithm " + algorithm, e); + } + } + + public static void main(String[] args) { + File file; + if (args.length > 0) + file = new File(args[0]); + else { + System.err.println("Usage: []" + " (see http://java.sun.com/j2se/1.5.0/" + + "docs/guide/security/CryptoSpec.html#AppA)"); + return; + } + + if (args.length > 1) { + String algorithm = args[1]; + System.out.println(digest(algorithm, file)); + } else { + String algorithm = "MD5"; + System.out.println(algorithm + ": " + digest(algorithm, file)); + algorithm = "SHA"; + System.out.println(algorithm + ": " + digest(algorithm, file)); + System.out.println(algorithm + ": " + sha1hex(file.toPath())); + algorithm = "SHA-256"; + System.out.println(algorithm + ": " + digest(algorithm, file)); + algorithm = "SHA-512"; + System.out.println(algorithm + ": " + digest(algorithm, file)); + } + } + + final private static char[] hexArray = "0123456789abcdef".toCharArray(); + + /** + * From + * http://stackoverflow.com/questions/9655181/how-to-convert-a-byte-array-to + * -a-hex-string-in-java + */ + public static String encodeHexString(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + +} diff --git a/org.argeo.util/src/org/argeo/util/DirH.java b/org.argeo.util/src/org/argeo/util/DirH.java new file mode 100644 index 000000000..b6d962f06 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/DirH.java @@ -0,0 +1,116 @@ +package org.argeo.util; + +import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.Charset; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** Hashes the hashes of the files in a directory. */ +public class DirH { + + private final static Charset charset = Charset.forName("UTF-16"); + private final static long bufferSize = 200 * 1024 * 1024; + private final static String algorithm = "SHA"; + + private final static byte EOL = (byte) '\n'; + private final static byte SPACE = (byte) ' '; + + private final int hashSize; + + private final byte[][] hashes; + private final byte[][] fileNames; + private final byte[] digest; + private final byte[] dirName; + + /** + * @param dirName can be null or empty + */ + private DirH(byte[][] hashes, byte[][] fileNames, byte[] dirName) { + if (hashes.length != fileNames.length) + throw new IllegalArgumentException(hashes.length + " hashes and " + fileNames.length + " file names"); + this.hashes = hashes; + this.fileNames = fileNames; + this.dirName = dirName == null ? new byte[0] : dirName; + if (hashes.length == 0) {// empty dir + hashSize = 20; + // FIXME what is the digest of an empty dir? + digest = new byte[hashSize]; + Arrays.fill(digest, SPACE); + return; + } + hashSize = hashes[0].length; + for (int i = 0; i < hashes.length; i++) { + if (hashes[i].length != hashSize) + throw new IllegalArgumentException( + "Hash size for " + new String(fileNames[i], charset) + " is " + hashes[i].length); + } + + try { + MessageDigest md = MessageDigest.getInstance(algorithm); + for (int i = 0; i < hashes.length; i++) { + md.update(this.hashes[i]); + md.update(SPACE); + md.update(this.fileNames[i]); + md.update(EOL); + } + digest = md.digest(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalArgumentException("Cannot digest", e); + } + } + + public void print(PrintStream out) { + out.print(DigestUtils.encodeHexString(digest)); + if (dirName.length > 0) { + out.print(' '); + out.print(new String(dirName, charset)); + } + out.print('\n'); + for (int i = 0; i < hashes.length; i++) { + out.print(DigestUtils.encodeHexString(hashes[i])); + out.print(' '); + out.print(new String(fileNames[i], charset)); + out.print('\n'); + } + } + + public static DirH digest(Path dir) { + try (DirectoryStream files = Files.newDirectoryStream(dir)) { + List hs = new ArrayList(); + List fNames = new ArrayList<>(); + for (Path file : files) { + if (!Files.isDirectory(file)) { + byte[] digest = DigestUtils.digestRaw(algorithm, file, bufferSize); + hs.add(digest); + fNames.add(file.getFileName().toString()); + } + } + + byte[][] fileNames = new byte[fNames.size()][]; + for (int i = 0; i < fNames.size(); i++) { + fileNames[i] = fNames.get(i).getBytes(charset); + } + byte[][] hashes = hs.toArray(new byte[hs.size()][]); + return new DirH(hashes, fileNames, dir.toString().getBytes(charset)); + } catch (IOException e) { + throw new RuntimeException("Cannot digest " + dir, e); + } + } + + public static void main(String[] args) { + try { + DirH dirH = DirH.digest(Paths.get("/home/mbaudier/tmp/")); + dirH.print(System.out); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/org.argeo.util/src/org/argeo/util/LangUtils.java b/org.argeo.util/src/org/argeo/util/LangUtils.java new file mode 100644 index 000000000..162294537 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/LangUtils.java @@ -0,0 +1,284 @@ +package org.argeo.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.time.ZonedDateTime; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.util.ArrayList; +import java.util.Dictionary; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; + +/** Utilities around Java basic features. */ +public class LangUtils { + /* + * NON-API OSGi + */ + /** + * Returns an array with the names of the provided classes. Useful when + * registering services with multiple interfaces in OSGi. + */ + public static String[] names(Class... clzz) { + String[] res = new String[clzz.length]; + for (int i = 0; i < clzz.length; i++) + res[i] = clzz[i].getName(); + return res; + } + + /* + * MAP + */ + /** + * Creates a new {@link Dictionary} with one key-value pair. Key should not be + * null, but if the value is null, it returns an empty {@link Dictionary}. + */ + public static Map map(String key, Object value) { + assert key != null; + HashMap props = new HashMap<>(); + if (value != null) + props.put(key, value); + return props; + } + + /* + * DICTIONARY + */ + + /** + * Creates a new {@link Dictionary} with one key-value pair. Key should not be + * null, but if the value is null, it returns an empty {@link Dictionary}. + */ + public static Dictionary dict(String key, Object value) { + assert key != null; + Hashtable props = new Hashtable<>(); + if (value != null) + props.put(key, value); + return props; + } + + /** @deprecated Use {@link #dict(String, Object)} instead. */ + @Deprecated + public static Dictionary dico(String key, Object value) { + return dict(key, value); + } + + /** Converts a {@link Dictionary} to a {@link Map} of strings. */ + public static Map dictToStringMap(Dictionary properties) { + if (properties == null) { + return null; + } + Map res = new HashMap<>(properties.size()); + Enumeration keys = properties.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + res.put(key, properties.get(key).toString()); + } + return res; + } + + /** + * Get a string property from this map, expecting to find it, or + * null if not found. + */ + public static String get(Map map, String key) { + Object res = map.get(key); + if (res == null) + return null; + return res.toString(); + } + + /** + * Get a string property from this map, expecting to find it. + * + * @throws IllegalArgumentException if the key was not found + */ + public static String getNotNull(Map map, String key) { + Object res = map.get(key); + if (res == null) + throw new IllegalArgumentException("Map " + map + " should contain key " + key); + return res.toString(); + } + + /** + * Wraps the keys of the provided {@link Dictionary} as an {@link Iterable}. + */ + public static Iterable keys(Dictionary props) { + assert props != null; + return new DictionaryKeys(props); + } + + static String toJson(Dictionary props) { + return toJson(props, false); + } + + static String toJson(Dictionary props, boolean pretty) { + StringBuilder sb = new StringBuilder(); + sb.append('{'); + if (pretty) + sb.append('\n'); + Enumeration keys = props.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + if (pretty) + sb.append(' '); + sb.append('\"').append(key).append('\"'); + if (pretty) + sb.append(" : "); + else + sb.append(':'); + sb.append('\"').append(props.get(key)).append('\"'); + if (keys.hasMoreElements()) + sb.append(", "); + if (pretty) + sb.append('\n'); + } + sb.append('}'); + return sb.toString(); + } + + static void storeAsProperties(Dictionary props, Path path) throws IOException { + if (props == null) + throw new IllegalArgumentException("Props cannot be null"); + Properties toStore = new Properties(); + for (Enumeration keys = props.keys(); keys.hasMoreElements();) { + String key = keys.nextElement(); + toStore.setProperty(key, props.get(key).toString()); + } + try (OutputStream out = Files.newOutputStream(path)) { + toStore.store(out, null); + } + } + + static void appendAsLdif(String dnBase, String dnKey, Dictionary props, Path path) + throws IOException { + if (props == null) + throw new IllegalArgumentException("Props cannot be null"); + Object dnValue = props.get(dnKey); + String dnStr = dnKey + '=' + dnValue + ',' + dnBase; + LdapName dn; + try { + dn = new LdapName(dnStr); + } catch (InvalidNameException e) { + throw new IllegalArgumentException("Cannot interpret DN " + dnStr, e); + } + if (dnValue == null) + throw new IllegalArgumentException("DN key " + dnKey + " must have a value"); + try (Writer writer = Files.newBufferedWriter(path, StandardOpenOption.APPEND, StandardOpenOption.CREATE)) { + writer.append("\ndn: "); + writer.append(dn.toString()); + writer.append('\n'); + for (Enumeration keys = props.keys(); keys.hasMoreElements();) { + String key = keys.nextElement(); + Object value = props.get(key); + writer.append(key); + writer.append(": "); + // FIXME deal with binary and multiple values + writer.append(value.toString()); + writer.append('\n'); + } + } + } + + static Dictionary loadFromProperties(Path path) throws IOException { + Properties toLoad = new Properties(); + try (InputStream in = Files.newInputStream(path)) { + toLoad.load(in); + } + Dictionary res = new Hashtable(); + for (Object key : toLoad.keySet()) + res.put(key.toString(), toLoad.get(key)); + return res; + } + + /* + * COLLECTIONS + */ + /** + * Convert a comma-separated separated {@link String} or a {@link String} array + * to a {@link List} of {@link String}, trimming them. Useful to quickly + * interpret OSGi services properties. + * + * @return a {@link List} containing the trimmed {@link String}s, or an empty + * {@link List} if the argument was null. + */ + public static List toStringList(Object value) { + List values = new ArrayList<>(); + if (value == null) + return values; + String[] arr; + if (value instanceof String) { + arr = ((String) value).split(","); + } else if (value instanceof String[]) { + arr = (String[]) value; + } else { + throw new IllegalArgumentException("Unsupported value type " + value.getClass()); + } + for (String str : arr) { + values.add(str.trim()); + } + return values; + } + + /* + * EXCEPTIONS + */ + /** + * Chain the messages of all causes (one per line, starts with a line + * return) without all the stack + */ + public static String chainCausesMessages(Throwable t) { + StringBuffer buf = new StringBuffer(); + chainCauseMessage(buf, t); + return buf.toString(); + } + + /** Recursive chaining of messages */ + private static void chainCauseMessage(StringBuffer buf, Throwable t) { + buf.append('\n').append(' ').append(t.getClass().getCanonicalName()).append(": ").append(t.getMessage()); + if (t.getCause() != null) + chainCauseMessage(buf, t.getCause()); + } + + /* + * TIME + */ + /** Formats time elapsed since start. */ + public static String since(ZonedDateTime start) { + ZonedDateTime now = ZonedDateTime.now(); + return duration(start, now); + } + + /** Formats a duration. */ + public static String duration(Temporal start, Temporal end) { + long count = ChronoUnit.DAYS.between(start, end); + if (count != 0) + return count > 1 ? count + " days" : count + " day"; + count = ChronoUnit.HOURS.between(start, end); + if (count != 0) + return count > 1 ? count + " hours" : count + " hours"; + count = ChronoUnit.MINUTES.between(start, end); + if (count != 0) + return count > 1 ? count + " minutes" : count + " minute"; + count = ChronoUnit.SECONDS.between(start, end); + return count > 1 ? count + " seconds" : count + " second"; + } + + /** Singleton constructor. */ + private LangUtils() { + + } + +} diff --git a/org.argeo.util/src/org/argeo/util/OS.java b/org.argeo.util/src/org/argeo/util/OS.java new file mode 100644 index 000000000..d8127b600 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/OS.java @@ -0,0 +1,56 @@ +package org.argeo.util; + +import java.io.File; +import java.lang.management.ManagementFactory; + +/** When OS specific informations are needed. */ +public class OS { + public final static OS LOCAL = new OS(); + + private final String arch, name, version; + + /** The OS of the running JVM */ + protected OS() { + arch = System.getProperty("os.arch"); + name = System.getProperty("os.name"); + version = System.getProperty("os.version"); + } + + public String getArch() { + return arch; + } + + public String getName() { + return name; + } + + public String getVersion() { + return version; + } + + public boolean isMSWindows() { + // only MS Windows would use such an horrendous separator... + return File.separatorChar == '\\'; + } + + public String[] getDefaultShellCommand() { + if (!isMSWindows()) + return new String[] { "/bin/sh", "-l", "-i" }; + else + return new String[] { "cmd.exe", "/C" }; + } + + public static Integer getJvmPid() { + /* + * This method works on most platforms (including Linux). Although when Java 9 + * comes along, there is a better way: long pid = + * ProcessHandle.current().getPid(); + * + * See: + * http://stackoverflow.com/questions/35842/how-can-a-java-program-get-its-own- + * process-id + */ + String pidAndHost = ManagementFactory.getRuntimeMXBean().getName(); + return Integer.parseInt(pidAndHost.substring(0, pidAndHost.indexOf('@'))); + } +} diff --git a/org.argeo.util/src/org/argeo/util/PasswordEncryption.java b/org.argeo.util/src/org/argeo/util/PasswordEncryption.java new file mode 100644 index 000000000..c95c7879e --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/PasswordEncryption.java @@ -0,0 +1,216 @@ +package org.argeo.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.Key; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +public class PasswordEncryption { + public final static Integer DEFAULT_ITERATION_COUNT = 1024; + /** Stronger with 256, but causes problem with Oracle JVM */ + public final static Integer DEFAULT_SECRETE_KEY_LENGTH = 256; + public final static Integer DEFAULT_SECRETE_KEY_LENGTH_RESTRICTED = 128; + public final static String DEFAULT_SECRETE_KEY_FACTORY = "PBKDF2WithHmacSHA1"; + public final static String DEFAULT_SECRETE_KEY_ENCRYPTION = "AES"; + public final static String DEFAULT_CIPHER_NAME = "AES/CBC/PKCS5Padding"; +// public final static String DEFAULT_CHARSET = "UTF-8"; + public final static Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; + + private Integer iterationCount = DEFAULT_ITERATION_COUNT; + private Integer secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH; + private String secreteKeyFactoryName = DEFAULT_SECRETE_KEY_FACTORY; + private String secreteKeyEncryption = DEFAULT_SECRETE_KEY_ENCRYPTION; + private String cipherName = DEFAULT_CIPHER_NAME; + + private static byte[] DEFAULT_SALT_8 = { (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56, + (byte) 0x35, (byte) 0xE3, (byte) 0x03 }; + private static byte[] DEFAULT_IV_16 = { (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56, + (byte) 0x35, (byte) 0xE3, (byte) 0x03, (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, (byte) 0x56, + (byte) 0x35, (byte) 0xE3, (byte) 0x03 }; + + private Key key; + private Cipher ecipher; + private Cipher dcipher; + + private String securityProviderName = null; + + /** + * This is up to the caller to clear the passed array. Neither copy of nor + * reference to the passed array is kept + */ + public PasswordEncryption(char[] password) { + this(password, DEFAULT_SALT_8, DEFAULT_IV_16); + } + + /** + * This is up to the caller to clear the passed array. Neither copies of nor + * references to the passed arrays are kept + */ + public PasswordEncryption(char[] password, byte[] passwordSalt, byte[] initializationVector) { + try { + initKeyAndCiphers(password, passwordSalt, initializationVector); + } catch (InvalidKeyException e) { + Integer previousSecreteKeyLength = secreteKeyLength; + secreteKeyLength = DEFAULT_SECRETE_KEY_LENGTH_RESTRICTED; + System.err.println("'" + e.getMessage() + "', will use " + secreteKeyLength + + " secrete key length instead of " + previousSecreteKeyLength); + try { + initKeyAndCiphers(password, passwordSalt, initializationVector); + } catch (GeneralSecurityException e1) { + throw new IllegalStateException("Cannot get secret key (with restricted length)", e1); + } + } catch (GeneralSecurityException e) { + throw new IllegalStateException("Cannot get secret key", e); + } + } + + protected void initKeyAndCiphers(char[] password, byte[] passwordSalt, byte[] initializationVector) + throws GeneralSecurityException { + byte[] salt = new byte[8]; + System.arraycopy(passwordSalt, 0, salt, 0, salt.length); + // for (int i = 0; i < password.length && i < salt.length; i++) + // salt[i] = (byte) password[i]; + byte[] iv = new byte[16]; + System.arraycopy(initializationVector, 0, iv, 0, iv.length); + + SecretKeyFactory keyFac = SecretKeyFactory.getInstance(getSecretKeyFactoryName()); + PBEKeySpec keySpec = new PBEKeySpec(password, salt, getIterationCount(), getKeyLength()); + String secKeyEncryption = getSecretKeyEncryption(); + if (secKeyEncryption != null) { + SecretKey tmp = keyFac.generateSecret(keySpec); + key = new SecretKeySpec(tmp.getEncoded(), getSecretKeyEncryption()); + } else { + key = keyFac.generateSecret(keySpec); + } + if (securityProviderName != null) + ecipher = Cipher.getInstance(getCipherName(), securityProviderName); + else + ecipher = Cipher.getInstance(getCipherName()); + ecipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv)); + dcipher = Cipher.getInstance(getCipherName()); + dcipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv)); + } + + public void encrypt(InputStream decryptedIn, OutputStream encryptedOut) throws IOException { + try { + CipherOutputStream out = new CipherOutputStream(encryptedOut, ecipher); + StreamUtils.copy(decryptedIn, out); + StreamUtils.closeQuietly(out); + } catch (IOException e) { + throw e; + } finally { + StreamUtils.closeQuietly(decryptedIn); + } + } + + public void decrypt(InputStream encryptedIn, OutputStream decryptedOut) throws IOException { + try { + CipherInputStream decryptedIn = new CipherInputStream(encryptedIn, dcipher); + StreamUtils.copy(decryptedIn, decryptedOut); + } catch (IOException e) { + throw e; + } finally { + StreamUtils.closeQuietly(encryptedIn); + } + } + + public byte[] encryptString(String str) { + ByteArrayOutputStream out = null; + ByteArrayInputStream in = null; + try { + out = new ByteArrayOutputStream(); + in = new ByteArrayInputStream(str.getBytes(DEFAULT_CHARSET)); + encrypt(in, out); + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + StreamUtils.closeQuietly(out); + } + } + + /** Closes the input stream */ + public String decryptAsString(InputStream in) { + ByteArrayOutputStream out = null; + try { + out = new ByteArrayOutputStream(); + decrypt(in, out); + return new String(out.toByteArray(), DEFAULT_CHARSET); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + StreamUtils.closeQuietly(out); + } + } + + protected Key getKey() { + return key; + } + + protected Cipher getEcipher() { + return ecipher; + } + + protected Cipher getDcipher() { + return dcipher; + } + + protected Integer getIterationCount() { + return iterationCount; + } + + protected Integer getKeyLength() { + return secreteKeyLength; + } + + protected String getSecretKeyFactoryName() { + return secreteKeyFactoryName; + } + + protected String getSecretKeyEncryption() { + return secreteKeyEncryption; + } + + protected String getCipherName() { + return cipherName; + } + + public void setIterationCount(Integer iterationCount) { + this.iterationCount = iterationCount; + } + + public void setSecreteKeyLength(Integer keyLength) { + this.secreteKeyLength = keyLength; + } + + public void setSecreteKeyFactoryName(String secreteKeyFactoryName) { + this.secreteKeyFactoryName = secreteKeyFactoryName; + } + + public void setSecreteKeyEncryption(String secreteKeyEncryption) { + this.secreteKeyEncryption = secreteKeyEncryption; + } + + public void setCipherName(String cipherName) { + this.cipherName = cipherName; + } + + public void setSecurityProviderName(String securityProviderName) { + this.securityProviderName = securityProviderName; + } +} diff --git a/org.argeo.util/src/org/argeo/util/ServiceChannel.java b/org.argeo.util/src/org/argeo/util/ServiceChannel.java new file mode 100644 index 000000000..799738414 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/ServiceChannel.java @@ -0,0 +1,78 @@ +package org.argeo.util; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousByteChannel; +import java.nio.channels.CompletionHandler; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; + +/** An {@link AsynchronousByteChannel} based on an {@link ExecutorService}. */ +public class ServiceChannel implements AsynchronousByteChannel { + private final ReadableByteChannel in; + private final WritableByteChannel out; + + private boolean open = true; + + private ExecutorService executor; + + public ServiceChannel(ReadableByteChannel in, WritableByteChannel out, ExecutorService executor) { + this.in = in; + this.out = out; + this.executor = executor; + } + + @Override + public Future read(ByteBuffer dst) { + return executor.submit(() -> in.read(dst)); + } + + @Override + public void read(ByteBuffer dst, A attachment, CompletionHandler handler) { + try { + Future res = read(dst); + handler.completed(res.get(), attachment); + } catch (Exception e) { + handler.failed(e, attachment); + } + } + + @Override + public Future write(ByteBuffer src) { + return executor.submit(() -> out.write(src)); + } + + @Override + public void write(ByteBuffer src, A attachment, CompletionHandler handler) { + try { + Future res = write(src); + handler.completed(res.get(), attachment); + } catch (Exception e) { + handler.failed(e, attachment); + } + } + + @Override + public synchronized void close() throws IOException { + try { + in.close(); + } catch (Exception e) { + e.printStackTrace(); + } + try { + out.close(); + } catch (Exception e) { + e.printStackTrace(); + } + open = false; + notifyAll(); + } + + @Override + public synchronized boolean isOpen() { + return open; + } + +} diff --git a/org.argeo.util/src/org/argeo/util/StreamUtils.java b/org.argeo.util/src/org/argeo/util/StreamUtils.java new file mode 100644 index 000000000..6d7d940ce --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/StreamUtils.java @@ -0,0 +1,81 @@ +package org.argeo.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; + +/** Utilities to be used when Apache Commons IO is not available. */ +class StreamUtils { + private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; + + /* + * APACHE COMMONS IO (inspired) + */ + + /** @return the number of bytes */ + public static Long copy(InputStream in, OutputStream out) + throws IOException { + Long count = 0l; + byte[] buf = new byte[DEFAULT_BUFFER_SIZE]; + while (true) { + int length = in.read(buf); + if (length < 0) + break; + out.write(buf, 0, length); + count = count + length; + } + return count; + } + + /** @return the number of chars */ + public static Long copy(Reader in, Writer out) throws IOException { + Long count = 0l; + char[] buf = new char[DEFAULT_BUFFER_SIZE]; + while (true) { + int length = in.read(buf); + if (length < 0) + break; + out.write(buf, 0, length); + count = count + length; + } + return count; + } + + public static void closeQuietly(InputStream in) { + if (in != null) + try { + in.close(); + } catch (Exception e) { + // + } + } + + public static void closeQuietly(OutputStream out) { + if (out != null) + try { + out.close(); + } catch (Exception e) { + // + } + } + + public static void closeQuietly(Reader in) { + if (in != null) + try { + in.close(); + } catch (Exception e) { + // + } + } + + public static void closeQuietly(Writer out) { + if (out != null) + try { + out.close(); + } catch (Exception e) { + // + } + } +} diff --git a/org.argeo.util/src/org/argeo/util/Tester.java b/org.argeo.util/src/org/argeo/util/Tester.java new file mode 100644 index 000000000..31a2be4ec --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/Tester.java @@ -0,0 +1,126 @@ +package org.argeo.util; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** A generic tester based on Java assertions and functional programming. */ +public class Tester { + private Map results = Collections.synchronizedSortedMap(new TreeMap<>()); + + private ClassLoader classLoader; + + /** Use {@link Thread#getContextClassLoader()} by default. */ + public Tester() { + this(Thread.currentThread().getContextClassLoader()); + } + + public Tester(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + public void execute(String className) { + Class clss; + try { + clss = classLoader.loadClass(className); + boolean assertionsEnabled = clss.desiredAssertionStatus(); + if (!assertionsEnabled) + throw new IllegalStateException("Test runner " + getClass().getName() + + " requires Java assertions to be enabled. Call the JVM with the -ea argument."); + } catch (Exception e1) { + throw new IllegalArgumentException("Cannot initalise test for " + className, e1); + + } + List methods = findMethods(clss); + if (methods.size() == 0) + throw new IllegalArgumentException("No test method found in " + clss); + // TODO make order more predictable? + for (Method method : methods) { + String uid = method.getDeclaringClass().getName() + "#" + method.getName(); + TesterStatus testStatus = new TesterStatus(uid); + Object obj = null; + try { + beforeTest(uid, method); + obj = clss.getDeclaredConstructor().newInstance(); + method.invoke(obj); + testStatus.setPassed(); + afterTestPassed(uid, method, obj); + } catch (Exception e) { + testStatus.setFailed(e); + afterTestFailed(uid, method, obj, e); + } finally { + results.put(uid, testStatus); + } + } + } + + protected void beforeTest(String uid, Method method) { + // System.out.println(uid + ": STARTING"); + } + + protected void afterTestPassed(String uid, Method method, Object obj) { + System.out.println(uid + ": PASSED"); + } + + protected void afterTestFailed(String uid, Method method, Object obj, Throwable e) { + System.out.println(uid + ": FAILED"); + e.printStackTrace(); + } + + protected List findMethods(Class clss) { + List methods = new ArrayList(); +// Method call = getMethod(clss, "call"); +// if (call != null) +// methods.add(call); +// + for (Method method : clss.getMethods()) { + if (method.getName().startsWith("test")) { + methods.add(method); + } + } + return methods; + } + + protected Method getMethod(Class clss, String name, Class... parameterTypes) { + try { + return clss.getMethod(name, parameterTypes); + } catch (NoSuchMethodException e) { + return null; + } catch (SecurityException e) { + throw new IllegalStateException(e); + } + } + + public static void main(String[] args) { + // deal with arguments + String className; + if (args.length < 1) { + System.err.println(usage()); + System.exit(1); + throw new IllegalArgumentException(); + } else { + className = args[0]; + } + + Tester test = new Tester(); + try { + test.execute(className); + } catch (Throwable e) { + e.printStackTrace(); + } + + Map r = test.results; + for (String uid : r.keySet()) { + TesterStatus testStatus = r.get(uid); + System.out.println(testStatus); + } + } + + public static String usage() { + return "java " + Tester.class.getName() + " [test class name]"; + + } +} diff --git a/org.argeo.util/src/org/argeo/util/TesterStatus.java b/org.argeo.util/src/org/argeo/util/TesterStatus.java new file mode 100644 index 000000000..d1d14ed06 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/TesterStatus.java @@ -0,0 +1,98 @@ +package org.argeo.util; + +import java.io.Serializable; + +/** The status of a test. */ +public class TesterStatus implements Serializable { + private static final long serialVersionUID = 6272975746885487000L; + + private Boolean passed = null; + private final String uid; + private Throwable throwable = null; + + public TesterStatus(String uid) { + this.uid = uid; + } + + /** For cloning. */ + public TesterStatus(String uid, Boolean passed, Throwable throwable) { + this(uid); + this.passed = passed; + this.throwable = throwable; + } + + public synchronized Boolean isRunning() { + return passed == null; + } + + public synchronized Boolean isPassed() { + assert passed != null; + return passed; + } + + public synchronized Boolean isFailed() { + assert passed != null; + return !passed; + } + + public synchronized void setPassed() { + setStatus(true); + } + + public synchronized void setFailed() { + setStatus(false); + } + + public synchronized void setFailed(Throwable throwable) { + setStatus(false); + setThrowable(throwable); + } + + protected void setStatus(Boolean passed) { + if (this.passed != null) + throw new IllegalStateException("Passed status of test " + uid + " is already set (to " + passed + ")"); + this.passed = passed; + } + + protected void setThrowable(Throwable throwable) { + if (this.throwable != null) + throw new IllegalStateException("Throwable of test " + uid + " is already set (to " + passed + ")"); + this.throwable = throwable; + } + + public String getUid() { + return uid; + } + + public Throwable getThrowable() { + return throwable; + } + + @Override + protected Object clone() throws CloneNotSupportedException { + // TODO Auto-generated method stub + return super.clone(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof TesterStatus) { + TesterStatus other = (TesterStatus) o; + // we don't check consistency for performance purposes + // this equals() is supposed to be used in collections or for transfer + return other.uid.equals(uid); + } + return false; + } + + @Override + public int hashCode() { + return uid.hashCode(); + } + + @Override + public String toString() { + return uid + "\t" + (passed ? "passed" : "failed"); + } + +} diff --git a/org.argeo.util/src/org/argeo/util/Throughput.java b/org.argeo.util/src/org/argeo/util/Throughput.java new file mode 100644 index 000000000..266ddbc58 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/Throughput.java @@ -0,0 +1,82 @@ +package org.argeo.util; + +import java.text.NumberFormat; +import java.text.ParseException; +import java.util.Locale; + +/** A throughput, that is, a value per unit of time. */ +public class Throughput { + private final static NumberFormat usNumberFormat = NumberFormat.getInstance(Locale.US); + + public enum Unit { + s, m, h, d + } + + private final Double value; + private final Unit unit; + + public Throughput(Double value, Unit unit) { + this.value = value; + this.unit = unit; + } + + public Throughput(Long periodMs, Long count, Unit unit) { + if (unit.equals(Unit.s)) + value = ((double) count * 1000d) / periodMs; + else if (unit.equals(Unit.m)) + value = ((double) count * 60d * 1000d) / periodMs; + else if (unit.equals(Unit.h)) + value = ((double) count * 60d * 60d * 1000d) / periodMs; + else if (unit.equals(Unit.d)) + value = ((double) count * 24d * 60d * 60d * 1000d) / periodMs; + else + throw new IllegalArgumentException("Unsupported unit " + unit); + this.unit = unit; + } + + public Throughput(Double value, String unitStr) { + this(value, Unit.valueOf(unitStr)); + } + + public Throughput(String def) { + int index = def.indexOf('/'); + if (def.length() < 3 || index <= 0 || index != def.length() - 2) + throw new IllegalArgumentException( + def + " no a proper throughput definition" + " (should be /, e.g. 3.54/s or 1500/h"); + String valueStr = def.substring(0, index); + String unitStr = def.substring(index + 1); + try { + this.value = usNumberFormat.parse(valueStr).doubleValue(); + } catch (ParseException e) { + throw new IllegalArgumentException("Cannot parse " + valueStr + " as a number.", e); + } + this.unit = Unit.valueOf(unitStr); + } + + public Long asMsPeriod() { + if (unit.equals(Unit.s)) + return Math.round(1000d / value); + else if (unit.equals(Unit.m)) + return Math.round((60d * 1000d) / value); + else if (unit.equals(Unit.h)) + return Math.round((60d * 60d * 1000d) / value); + else if (unit.equals(Unit.d)) + return Math.round((24d * 60d * 60d * 1000d) / value); + else + throw new IllegalArgumentException("Unsupported unit " + unit); + } + + @Override + public String toString() { + return usNumberFormat.format(value) + '/' + unit; + } + + public Double getValue() { + return value; + } + + public Unit getUnit() { + return unit; + } + +} diff --git a/org.argeo.util/src/org/argeo/util/UuidUtils.java b/org.argeo.util/src/org/argeo/util/UuidUtils.java new file mode 100644 index 000000000..7584abc1b --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/UuidUtils.java @@ -0,0 +1,378 @@ +package org.argeo.util; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.security.SecureRandom; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.BitSet; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Utilities to simplify and extends usage of {@link UUID}. Only the RFC 4122 + * variant (also known as Leach–Salz variant) is supported. + */ +public class UuidUtils { + /** Nil UUID (00000000-0000-0000-0000-000000000000). */ + public final static UUID NIL_UUID = UUID.fromString("00000000-0000-0000-0000-000000000000"); + public final static LocalDateTime GREGORIAN_START = LocalDateTime.of(1582, 10, 15, 0, 0, 0); + + private final static long MOST_SIG_VERSION1 = (1l << 12); + private final static long LEAST_SIG_RFC4122_VARIANT = (1l << 63); + + private final static SecureRandom RANDOM; + private final static AtomicInteger CLOCK_SEQUENCE; + private final static byte[] HARDWARE_ADDRESS; + /** A start timestamp to which {@link System#nanoTime()}/100 can be added. */ + private final static long START_TIMESTAMP; + static { + RANDOM = new SecureRandom(); + CLOCK_SEQUENCE = new AtomicInteger(RANDOM.nextInt(16384)); + HARDWARE_ADDRESS = getHardwareAddress(); + + long nowVm = System.nanoTime() / 100; + Duration duration = Duration.between(GREGORIAN_START, LocalDateTime.now(ZoneOffset.UTC)); + START_TIMESTAMP = (duration.getSeconds() * 10000000 + duration.getNano() / 100) - nowVm; + } + + private static byte[] getHardwareAddress() { + InetAddress localHost; + try { + localHost = InetAddress.getLocalHost(); + try { + NetworkInterface nic = NetworkInterface.getByInetAddress(localHost); + return nic.getHardwareAddress(); + } catch (SocketException e) { + return null; + } + } catch (UnknownHostException e) { + return null; + } + + } + + public static UUID timeUUIDwithRandomNode() { + long timestamp = START_TIMESTAMP + System.nanoTime() / 100; + return timeUUID(timestamp, RANDOM); + } + + public static UUID timeUUID(long timestamp, Random random) { + byte[] node = new byte[6]; + random.nextBytes(node); + node[0] = (byte) (node[0] | 1); + long clockSequence = CLOCK_SEQUENCE.incrementAndGet(); + return timeUUID(timestamp, clockSequence, node); + } + + public static UUID timeUUID() { + long timestamp = START_TIMESTAMP + System.nanoTime() / 100; + return timeUUID(timestamp); + } + + public static UUID timeUUID(long timestamp) { + if (HARDWARE_ADDRESS == null) + return timeUUID(timestamp, RANDOM); + long clockSequence = CLOCK_SEQUENCE.incrementAndGet(); + return timeUUID(timestamp, clockSequence, HARDWARE_ADDRESS); + } + + public static UUID timeUUID(long timestamp, NetworkInterface nic) { + byte[] node; + try { + node = nic.getHardwareAddress(); + } catch (SocketException e) { + throw new IllegalStateException("Cannot get hardware address", e); + } + long clockSequence = CLOCK_SEQUENCE.incrementAndGet(); + return timeUUID(timestamp, clockSequence, node); + } + + public static UUID timeUUID(LocalDateTime time, long clockSequence, byte[] node) { + Duration duration = Duration.between(GREGORIAN_START, time); + // Number of 100 ns intervals in one second: 1000000000 / 100 = 10000000 + long timestamp = duration.getSeconds() * 10000000 + duration.getNano() / 100; + return timeUUID(timestamp, clockSequence, node); + } + + public static UUID timeUUID(long timestamp, long clockSequence, byte[] node) { + assert node.length >= 6; + + long mostSig = MOST_SIG_VERSION1 // base for version 1 UUID + | ((timestamp & 0xFFFFFFFFL) << 32) // time_low + | (((timestamp >> 32) & 0xFFFFL) << 16) // time_mid + | ((timestamp >> 48) & 0x0FFFL);// time_hi_and_version + + long leastSig = LEAST_SIG_RFC4122_VARIANT // base for Leach–Salz UUID + | (((clockSequence & 0x3F00) >> 8) << 56) // clk_seq_hi_res + | ((clockSequence & 0xFF) << 48) // clk_seq_low + | (node[0] & 0xFFL) // + | ((node[1] & 0xFFL) << 8) // + | ((node[2] & 0xFFL) << 16) // + | ((node[3] & 0xFFL) << 24) // + | ((node[4] & 0xFFL) << 32) // + | ((node[5] & 0xFFL) << 40); // +// for (int i = 0; i < 6; i++) { +// leastSig = leastSig | ((node[i] & 0xFFL) << (8 * i)); +// } + UUID uuid = new UUID(mostSig, leastSig); + + // tests + assert uuid.node() == BitSet.valueOf(node).toLongArray()[0]; + assert uuid.timestamp() == timestamp; + assert uuid.clockSequence() == clockSequence; + assert uuid.version() == 1; + assert uuid.variant() == 2; + return uuid; + } + + @Deprecated + public static UUID timeBasedUUID() { + return timeBasedUUID(LocalDateTime.now(ZoneOffset.UTC)); + } + + @Deprecated + public static UUID timeBasedRandomUUID() { + return timeBasedRandomUUID(LocalDateTime.now(ZoneOffset.UTC), RANDOM); + } + + @Deprecated + public static UUID timeBasedUUID(LocalDateTime time) { + if (HARDWARE_ADDRESS == null) + return timeBasedRandomUUID(time, RANDOM); + return timeBasedUUID(time, BitSet.valueOf(HARDWARE_ADDRESS)); + } + + @Deprecated + public static UUID timeBasedAddressUUID(LocalDateTime time, NetworkInterface nic) throws SocketException { + byte[] nodeBytes = nic.getHardwareAddress(); + BitSet node = BitSet.valueOf(nodeBytes); + return timeBasedUUID(time, node); + } + + @Deprecated + public static UUID timeBasedRandomUUID(LocalDateTime time, Random random) { + byte[] nodeBytes = new byte[6]; + random.nextBytes(nodeBytes); + BitSet node = BitSet.valueOf(nodeBytes); + // set random marker + node.set(0, true); + return timeBasedUUID(time, node); + } + + @Deprecated + public static UUID timeBasedUUID(LocalDateTime time, BitSet node) { + // most significant + Duration duration = Duration.between(GREGORIAN_START, time); + + // Number of 100 ns intervals in one second: 1000000000 / 100 = 10000000 + long timeNanos = duration.getSeconds() * 10000000 + duration.getNano() / 100; + BitSet timeBits = BitSet.valueOf(new long[] { timeNanos }); + assert timeBits.length() <= 60; + + int clockSequence; + synchronized (CLOCK_SEQUENCE) { + clockSequence = CLOCK_SEQUENCE.incrementAndGet(); + if (clockSequence > 16384) + CLOCK_SEQUENCE.set(0); + } + BitSet clockSequenceBits = BitSet.valueOf(new long[] { clockSequence }); + + // Build the UUID, bit by bit + // see https://tools.ietf.org/html/rfc4122#section-4.2.2 + // time + BitSet time_low = new BitSet(32); + BitSet time_mid = new BitSet(16); + BitSet time_hi_and_version = new BitSet(16); + + for (int i = 0; i < 60; i++) { + if (i < 32) + time_low.set(i, timeBits.get(i)); + else if (i < 48) + time_mid.set(i - 32, timeBits.get(i)); + else + time_hi_and_version.set(i - 48, timeBits.get(i)); + } + // version + time_hi_and_version.set(12, true); + time_hi_and_version.set(13, false); + time_hi_and_version.set(14, false); + time_hi_and_version.set(15, false); + + // clock sequence + BitSet clk_seq_hi_res = new BitSet(8); + BitSet clk_seq_low = new BitSet(8); + for (int i = 0; i < 8; i++) { + clk_seq_low.set(i, clockSequenceBits.get(i)); + } + for (int i = 8; i < 14; i++) { + clk_seq_hi_res.set(i - 8, clockSequenceBits.get(i)); + } + // variant + clk_seq_hi_res.set(6, false); + clk_seq_hi_res.set(7, true); + +// String str = toHexString(time_low.toLongArray()[0]) + "-" + toHexString(time_mid.toLongArray()[0]) + "-" +// + toHexString(time_hi_and_version.toLongArray()[0]) + "-" +// + toHexString(clock_seq_hi_and_reserved.toLongArray()[0]) + toHexString(clock_seq_low.toLongArray()[0]) +// + "-" + toHexString(node.toLongArray()[0]); +// UUID uuid = UUID.fromString(str); + + BitSet uuidBits = new BitSet(128); + for (int i = 0; i < 128; i++) { + if (i < 48) + uuidBits.set(i, node.get(i)); + else if (i < 56) + uuidBits.set(i, clk_seq_low.get(i - 48)); + else if (i < 64) + uuidBits.set(i, clk_seq_hi_res.get(i - 56)); + else if (i < 80) + uuidBits.set(i, time_hi_and_version.get(i - 64)); + else if (i < 96) + uuidBits.set(i, time_mid.get(i - 80)); + else + uuidBits.set(i, time_low.get(i - 96)); + } + + long[] uuidLongs = uuidBits.toLongArray(); + assert uuidLongs.length == 2; + UUID uuid = new UUID(uuidLongs[1], uuidLongs[0]); + + // tests + assert uuid.node() == node.toLongArray()[0]; + assert uuid.timestamp() == timeNanos; + assert uuid.clockSequence() == clockSequence; + assert uuid.version() == 1; + assert uuid.variant() == 2; + return uuid; + } + + public static String toBinaryString(UUID uuid, int charsPerSegment, char separator) { + String binaryString = toBinaryString(uuid); + StringBuilder sb = new StringBuilder(128 + (128 / charsPerSegment)); + for (int i = 0; i < binaryString.length(); i++) { + if (i != 0 && i % charsPerSegment == 0) + sb.append(separator); + sb.append(binaryString.charAt(i)); + } + return sb.toString(); + } + + public static String toBinaryString(UUID uuid) { + String most = zeroTo64Chars(Long.toBinaryString(uuid.getMostSignificantBits())); + String least = zeroTo64Chars(Long.toBinaryString(uuid.getLeastSignificantBits())); + String binaryString = most + least; + assert binaryString.length() == 128; + return binaryString; + } + + private static String zeroTo64Chars(String str) { + assert str.length() <= 64; + if (str.length() < 64) { + StringBuilder sb = new StringBuilder(64); + for (int i = 0; i < 64 - str.length(); i++) + sb.append('0'); + sb.append(str); + return sb.toString(); + } else + return str; + } + + public static String compactToStd(String compact) { + if (compact.length() != 32) + throw new IllegalArgumentException( + "Compact UUID '" + compact + "' has length " + compact.length() + " and not 32."); + StringBuilder sb = new StringBuilder(36); + for (int i = 0; i < 32; i++) { + if (i == 8 || i == 12 || i == 16 || i == 20) + sb.append('-'); + sb.append(compact.charAt(i)); + } + String std = sb.toString(); + assert std.length() == 36; + assert UUID.fromString(std).toString().equals(std); + return std; + } + + public static UUID compactToUuid(String compact) { + return UUID.fromString(compactToStd(compact)); + } + + public static String firstBlock(UUID uuid) { + return uuid.toString().substring(0, 8); + } + + public static boolean isRandom(UUID uuid) { + return uuid.version() == 4; + } + + public static boolean isTimeBased(UUID uuid) { + return uuid.version() == 1; + } + + public static boolean isTimeBasedRandom(UUID uuid) { + if (uuid.version() == 1) { + BitSet node = BitSet.valueOf(new long[] { uuid.node() }); + return node.get(0); + } else + return false; + } + + public static boolean isNameBased(UUID uuid) { + return uuid.version() == 3 || uuid.version() == 5; + } + + /** Singleton. */ + private UuidUtils() { + } + + public final static void main(String[] args) throws Exception { + UUID uuid; + +// uuid = compactToUuid("996b1f5122de4b2f94e49168d32f22d1"); +// System.out.println(uuid.toString() + ", isRandom=" + isRandom(uuid)); + + // warm up before measuring perf + for (int i = 0; i < 10; i++) { + UUID.randomUUID(); + timeUUID(); + timeUUIDwithRandomNode(); + timeBasedRandomUUID(); + timeBasedUUID(); + } + + long begin; + long duration; + + begin = System.nanoTime(); + uuid = UUID.randomUUID(); + duration = System.nanoTime() - begin; + System.out.println(uuid.toString() + " in " + duration + " ns, isRandom=" + isRandom(uuid)); + + begin = System.nanoTime(); + uuid = timeUUID(); + duration = System.nanoTime() - begin; + System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid)); + + begin = System.nanoTime(); + uuid = timeUUIDwithRandomNode(); + duration = System.nanoTime() - begin; + System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid)); + + begin = System.nanoTime(); + uuid = timeBasedUUID(); + duration = System.nanoTime() - begin; + System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid)); + + begin = System.nanoTime(); + uuid = timeBasedRandomUUID(); + duration = System.nanoTime() - begin; + System.out.println(uuid.toString() + " in " + duration + " ns, isTimeBasedRandom=" + isTimeBasedRandom(uuid)); +// System.out.println(toBinaryString(uuid, 8, ' ')); +// System.out.println(toBinaryString(uuid, 16, '\n')); + } +} diff --git a/org.argeo.util/src/org/argeo/util/package-info.java b/org.argeo.util/src/org/argeo/util/package-info.java new file mode 100644 index 000000000..4354b0a14 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/package-info.java @@ -0,0 +1,2 @@ +/** Generic Java utilities. */ +package org.argeo.util; \ No newline at end of file diff --git a/org.argeo.util/src/org/argeo/util/register/Component.java b/org.argeo.util/src/org/argeo/util/register/Component.java new file mode 100644 index 000000000..4a812f83b --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/register/Component.java @@ -0,0 +1,283 @@ +package org.argeo.util.register; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +/** + * A wrapper for an object, whose dependencies and life cycle can be managed. + */ +public class Component { + + private final I instance; + + private final Runnable init; + private final Runnable close; + + private final Map, PublishedType> types; + private final Set> dependencies; + + private CompletableFuture activationStarted = null; + private CompletableFuture activated = null; + + private CompletableFuture deactivationStarted = null; + private CompletableFuture deactivated = null; + + private Set> dependants = new HashSet<>(); + + Component(Consumer> register, I instance, Runnable init, Runnable close, + Set> dependencies, Set> classes) { + assert instance != null; + assert init != null; + assert close != null; + assert dependencies != null; + assert classes != null; + + this.instance = instance; + this.init = init; + this.close = close; + + // types + Map, PublishedType> types = new HashMap<>(classes.size()); + for (Class clss : classes) { +// if (!clss.isAssignableFrom(instance.getClass())) +// throw new IllegalArgumentException( +// "Type " + clss.getName() + " is not compatible with " + instance.getClass().getName()); + types.put(clss, new PublishedType<>(this, clss)); + } + this.types = Collections.unmodifiableMap(types); + + // dependencies + this.dependencies = Collections.unmodifiableSet(new HashSet<>(dependencies)); + for (Dependency dependency : this.dependencies) { + dependency.setDependantComponent(this); + } + + // deactivated by default + deactivated = CompletableFuture.completedFuture(null); + deactivationStarted = CompletableFuture.completedFuture(null); + + // TODO check whether context is active, so that we start right away + prepareNextActivation(); + + register.accept(this); + } + + private void prepareNextActivation() { + activationStarted = new CompletableFuture(); + activated = activationStarted // + .thenComposeAsync(this::dependenciesActivated) // + .thenRun(this.init) // + .thenRun(() -> prepareNextDeactivation()); + } + + private void prepareNextDeactivation() { + deactivationStarted = new CompletableFuture(); + deactivated = deactivationStarted // + .thenComposeAsync(this::dependantsDeactivated) // + .thenRun(this.close) // + .thenRun(() -> prepareNextActivation()); + } + + public CompletableFuture getActivated() { + return activated; + } + + public CompletableFuture getDeactivated() { + return deactivated; + } + + void startActivating() { + if (activated.isDone() || activationStarted.isDone()) + return; + activationStarted.complete(null); + } + + void startDeactivating() { + if (deactivated.isDone() || deactivationStarted.isDone()) + return; + deactivationStarted.complete(null); + } + + CompletableFuture dependenciesActivated(Void v) { + Set> constraints = new HashSet<>(this.dependencies.size()); + for (Dependency dependency : this.dependencies) { + CompletableFuture dependencyActivated = dependency.publisherActivated() // + .thenCompose(dependency::set); + constraints.add(dependencyActivated); + } + return CompletableFuture.allOf(constraints.toArray(new CompletableFuture[constraints.size()])); + } + + CompletableFuture dependantsDeactivated(Void v) { + Set> constraints = new HashSet<>(this.dependants.size()); + for (Dependency dependant : this.dependants) { + CompletableFuture dependantDeactivated = dependant.dependantDeactivated() // + .thenCompose(dependant::unset); + constraints.add(dependantDeactivated); + } + CompletableFuture dependantsDeactivated = CompletableFuture + .allOf(constraints.toArray(new CompletableFuture[constraints.size()])); + return dependantsDeactivated; + + } + + void addDependant(Dependency dependant) { + dependants.add(dependant); + } + + I getInstance() { + return instance; + } + + @SuppressWarnings("unchecked") + PublishedType getType(Class clss) { + if (!types.containsKey(clss)) + throw new IllegalArgumentException(clss.getName() + " is not a type published by this component"); + return (PublishedType) types.get(clss); + } + + boolean isPublishedType(Class clss) { + return types.containsKey(clss); + } + + public static class PublishedType { + private Component component; + private Class clss; + +// private CompletableFuture> publisherAvailable; + private CompletableFuture value; + + public PublishedType(Component component, Class clss) { + this.clss = clss; + this.component = component; + value = CompletableFuture.completedFuture((T) component.instance); +// value = publisherAvailable.thenApply((c) -> c.getInstance()); + } + + Component getPublisher() { + return component; + } + +// CompletableFuture> publisherAvailable() { +// return publisherAvailable; +// } + + Class getType() { + return clss; + } + } + + public static class Builder { + private final I instance; + + private Runnable init; + private Runnable close; + + private Set> dependencies = new HashSet<>(); + private Set> types = new HashSet<>(); + + public Builder(I instance) { + this.instance = instance; + } + + public Component build(Consumer> register) { + // default values + if (types.isEmpty()) { + types.add(getInstanceClass()); + } + + if (init == null) + init = () -> { + }; + if (close == null) + close = () -> { + }; + + // instantiation + Component component = new Component(register, instance, init, close, dependencies, types); + for (Dependency dependency : dependencies) { + dependency.type.getPublisher().addDependant(dependency); + } + return component; + } + + public Builder addType(Class clss) { + types.add(clss); + return this; + } + + public Builder addInit(Runnable init) { + if (this.init != null) + throw new IllegalArgumentException("init method is already set"); + this.init = init; + return this; + } + + public Builder addClose(Runnable close) { + if (this.close != null) + throw new IllegalArgumentException("close method is already set"); + this.close = close; + return this; + } + + public Builder addDependency(PublishedType type, Consumer set, Consumer unset) { + dependencies.add(new Dependency(type, set, unset)); + return this; + } + + public I get() { + return instance; + } + + @SuppressWarnings("unchecked") + private Class getInstanceClass() { + return (Class) instance.getClass(); + } + + } + + static class Dependency { + private PublishedType type; + private Consumer set; + private Consumer unset; + + // live + Component dependantComponent; + CompletableFuture setStage; + CompletableFuture unsetStage; + + public Dependency(PublishedType types, Consumer set, Consumer unset) { + super(); + this.type = types; + this.set = set; + this.unset = unset != null ? unset : (v) -> set.accept(null); + } + + // live + void setDependantComponent(Component component) { + this.dependantComponent = component; + } + + CompletableFuture publisherActivated() { + return type.getPublisher().activated.copy(); + } + + CompletableFuture dependantDeactivated() { + return dependantComponent.deactivated.copy(); + } + + CompletableFuture set(Void v) { + return type.value.thenAccept(set); + } + + CompletableFuture unset(Void v) { + return type.value.thenAccept(unset); + } + + } +} diff --git a/org.argeo.util/src/org/argeo/util/register/Register.java b/org.argeo.util/src/org/argeo/util/register/Register.java new file mode 100644 index 000000000..17062809e --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/register/Register.java @@ -0,0 +1,10 @@ +package org.argeo.util.register; + +import java.util.Map; + +/** A dynamic register of objects. */ +public interface Register { + Singleton set(T obj, Class clss, Map attributes, Class... classes); + + void shutdown(); +} diff --git a/org.argeo.util/src/org/argeo/util/register/Singleton.java b/org.argeo.util/src/org/argeo/util/register/Singleton.java new file mode 100644 index 000000000..5d70e9aeb --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/register/Singleton.java @@ -0,0 +1,41 @@ +package org.argeo.util.register; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.Future; +import java.util.function.Consumer; + +public class Singleton { + private final Class clss; + private final CompletableFuture registrationStage; + private final List> unregistrationHooks = new ArrayList<>(); + + public Singleton(Class clss, CompletableFuture registrationStage) { + this.clss = clss; + this.registrationStage = registrationStage; + } + + CompletionStage getRegistrationStage() { + return registrationStage.minimalCompletionStage(); + } + + public void addUnregistrationHook(Consumer todo) { + unregistrationHooks.add(todo); + } + + public Future getValue() { + return registrationStage.copy(); + } + + public CompletableFuture prepareUnregistration(Void v) { + List> lst = new ArrayList<>(); + for (Consumer hook : unregistrationHooks) { + lst.add(registrationStage.thenAcceptAsync(hook)); + } + CompletableFuture prepareUnregistrationStage = CompletableFuture + .allOf(lst.toArray(new CompletableFuture[lst.size()])); + return prepareUnregistrationStage; + } +} diff --git a/org.argeo.util/src/org/argeo/util/register/StaticRegister.java b/org.argeo.util/src/org/argeo/util/register/StaticRegister.java new file mode 100644 index 000000000..c186aff08 --- /dev/null +++ b/org.argeo.util/src/org/argeo/util/register/StaticRegister.java @@ -0,0 +1,97 @@ +package org.argeo.util.register; + +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** A minimal component register. */ +public class StaticRegister { + private final static AtomicBoolean started = new AtomicBoolean(false); + private final static IdentityHashMap> components = new IdentityHashMap<>(); + + public static Consumer> asConsumer() { + return (c) -> registerComponent(c); + } + +// public static BiFunction, Predicate>, Component> asProvider() { +// +// } + + static synchronized Component find(Class clss, Predicate> filter) { + Set> result = new HashSet<>(); + instances: for (Object instance : components.keySet()) { + if (!clss.isAssignableFrom(instance.getClass())) + continue instances; + Component component = (Component) components.get(instance); + + // TODO filter + if (component.isPublishedType(clss)) + result.add(component); + } + if (result.isEmpty()) + return null; + return result.iterator().next(); + + } + + static synchronized void registerComponent(Component component) { + if (started.get()) // TODO make it really dynamic + throw new IllegalStateException("Already activated"); + if (components.containsKey(component.getInstance())) + throw new IllegalArgumentException("Already registered as component"); + components.put(component.getInstance(), component); + } + + static synchronized Component get(Object instance) { + if (!components.containsKey(instance)) + throw new IllegalArgumentException("Not registered as component"); + return components.get(instance); + } + + synchronized static void activate() { + if (started.get()) + throw new IllegalStateException("Already activated"); + Set> constraints = new HashSet<>(); + for (Component component : components.values()) { + component.startActivating(); + constraints.add(component.getActivated()); + } + + // wait + try { + CompletableFuture.allOf(constraints.toArray(new CompletableFuture[0])).thenRun(() -> started.set(true)) + .get(); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + synchronized static void deactivate() { + if (!started.get()) + throw new IllegalStateException("Not activated"); + Set> constraints = new HashSet<>(); + for (Component component : components.values()) { + component.startDeactivating(); + constraints.add(component.getDeactivated()); + } + + // wait + try { + CompletableFuture.allOf(constraints.toArray(new CompletableFuture[0])).thenRun(() -> started.set(false)) + .get(); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException(e); + } + } + + synchronized static void clear() { + components.clear(); + } + +} diff --git a/pom.xml b/pom.xml index 619b9a73e..5113e3462 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ org.argeo.init - org.argeo.enterprise + org.argeo.util org.argeo.api @@ -37,7 +37,6 @@ rap dep - demo dist sdk