X-Git-Url: http://git.argeo.org/?a=blobdiff_plain;f=org.argeo.security.core%2Fsrc%2Forg%2Fargeo%2Fosgi%2Fuseradmin%2FLdifUser.java;h=866c48c6b4c8d64d0abfe879e9482c8fd74770a9;hb=8260f4470f514ea347ca53f5b4dfc632c4a4de66;hp=9f378f1510163396fb8ccb2cbcc965e929bb360a;hpb=270c84f092b77b6f101a742cff565d29ee756011;p=lgpl%2Fargeo-commons.git diff --git a/org.argeo.security.core/src/org/argeo/osgi/useradmin/LdifUser.java b/org.argeo.security.core/src/org/argeo/osgi/useradmin/LdifUser.java index 9f378f151..866c48c6b 100644 --- a/org.argeo.security.core/src/org/argeo/osgi/useradmin/LdifUser.java +++ b/org.argeo.security.core/src/org/argeo/osgi/useradmin/LdifUser.java @@ -1,35 +1,50 @@ package org.argeo.osgi.useradmin; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; 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.osgi.service.useradmin.User; - -class LdifUser implements User { - // optimisation - List directMemberOf = new ArrayList(); +/** Directory user implementation */ +class LdifUser implements DirectoryUser { + private final AbstractUserDirectory userAdmin; private final LdapName dn; - private Attributes attributes; + + private final boolean frozen; + private Attributes publishedAttributes; private final AttributeDictionary properties; private final AttributeDictionary credentials; - private List credentialAttributes = Arrays - .asList(new String[] { "userpassword" }); + LdifUser(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes) { + this(userAdmin, dn, attributes, false); + } - LdifUser(LdapName dn, Attributes attributes) { + private LdifUser(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes, boolean frozen) { + this.userAdmin = userAdmin; this.dn = dn; - this.attributes = attributes; - properties = new AttributeDictionary(attributes, credentialAttributes, - false); - credentials = new AttributeDictionary(attributes, credentialAttributes, - true); + this.publishedAttributes = attributes; + properties = new AttributeDictionary(false); + credentials = new AttributeDictionary(true); + this.frozen = frozen; } @Override @@ -54,6 +69,13 @@ class LdifUser implements User { @Override public boolean hasCredential(String key, Object value) { + if (key == null) { + // TODO check other sources (like PKCS12) + char[] password = toChars(value); + byte[] hashedPassword = hash(password); + return hasCredential(LdifName.userPassword.name(), hashedPassword); + } + Object storedValue = getCredentials().get(key); if (storedValue == null || value == null) return false; @@ -66,12 +88,77 @@ class LdifUser implements User { return false; } - protected LdapName getDn() { + /** Hash and clear the password */ + private byte[] hash(char[] password) { + byte[] hashedPassword = ("{SHA}" + Base64.getEncoder().encodeToString(DigestUtils.sha1(toBytes(password)))) + .getBytes(); + Arrays.fill(password, '\u0000'); + return hashedPassword; + } + + private byte[] toBytes(char[] chars) { + CharBuffer charBuffer = CharBuffer.wrap(chars); + ByteBuffer byteBuffer = Charset.forName("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; + } + + private char[] toChars(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 = Charset.forName("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; + } + + @Override + public LdapName getDn() { return dn; } - protected Attributes getAttributes() { - return attributes; + @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 @@ -94,4 +181,183 @@ class LdifUser implements User { 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(LdifName.userPassword.name())) + // TODO other cases (certificates, images) + return value; + value = new String((byte[]) value, Charset.forName("UTF-8")); + } + if (attr.size() == 1) + return value; + if (!attr.getID().equals(LdifName.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 = toChars(value); + byte[] hashedPassword = hash(password); + return put(LdifName.userPassword.name(), hashedPassword); + } + + 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()); + attribute = new BasicAttribute(key.toString()); + if (value instanceof String && !isAsciiPrintable(((String) value))) + try { + attribute.add(((String) value).getBytes("UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new UserDirectoryException("Cannot encode " + value, e); + } + 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; + } + }