1 package org
.argeo
.osgi
.useradmin
;
3 import java
.io
.UnsupportedEncodingException
;
4 import java
.nio
.ByteBuffer
;
5 import java
.nio
.CharBuffer
;
6 import java
.nio
.charset
.Charset
;
7 import java
.util
.ArrayList
;
8 import java
.util
.Arrays
;
9 import java
.util
.Base64
;
10 import java
.util
.Collections
;
11 import java
.util
.Dictionary
;
12 import java
.util
.Enumeration
;
13 import java
.util
.HashSet
;
14 import java
.util
.Iterator
;
15 import java
.util
.List
;
18 import javax
.naming
.NamingEnumeration
;
19 import javax
.naming
.NamingException
;
20 import javax
.naming
.directory
.Attribute
;
21 import javax
.naming
.directory
.Attributes
;
22 import javax
.naming
.directory
.BasicAttribute
;
23 import javax
.naming
.ldap
.LdapName
;
25 import org
.argeo
.naming
.LdapAttrs
;
27 /** Directory user implementation */
28 class LdifUser
implements DirectoryUser
{
29 private final AbstractUserDirectory userAdmin
;
31 private final LdapName dn
;
33 private final boolean frozen
;
34 private Attributes publishedAttributes
;
36 private final AttributeDictionary properties
;
37 private final AttributeDictionary credentials
;
39 LdifUser(AbstractUserDirectory userAdmin
, LdapName dn
, Attributes attributes
) {
40 this(userAdmin
, dn
, attributes
, false);
43 private LdifUser(AbstractUserDirectory userAdmin
, LdapName dn
, Attributes attributes
, boolean frozen
) {
44 this.userAdmin
= userAdmin
;
46 this.publishedAttributes
= attributes
;
47 properties
= new AttributeDictionary(false);
48 credentials
= new AttributeDictionary(true);
53 public String
getName() {
58 public int getType() {
63 public Dictionary
<String
, Object
> getProperties() {
68 public Dictionary
<String
, Object
> getCredentials() {
73 public boolean hasCredential(String key
, Object value
) {
75 // TODO check other sources (like PKCS12)
76 char[] password
= toChars(value
);
77 byte[] hashedPassword
= hash(password
);
78 return hasCredential(LdapAttrs
.userPassword
.name(), hashedPassword
);
81 Object storedValue
= getCredentials().get(key
);
82 if (storedValue
== null || value
== null)
84 if (!(value
instanceof String
|| value
instanceof byte[]))
86 if (storedValue
instanceof String
&& value
instanceof String
)
87 return storedValue
.equals(value
);
88 if (storedValue
instanceof byte[] && value
instanceof byte[])
89 return Arrays
.equals((byte[]) storedValue
, (byte[]) value
);
93 /** Hash and clear the password */
94 private byte[] hash(char[] password
) {
95 byte[] hashedPassword
= ("{SHA}" + Base64
.getEncoder().encodeToString(DigestUtils
.sha1(toBytes(password
))))
97 Arrays
.fill(password
, '\u0000');
98 return hashedPassword
;
101 private byte[] toBytes(char[] chars
) {
102 CharBuffer charBuffer
= CharBuffer
.wrap(chars
);
103 ByteBuffer byteBuffer
= Charset
.forName("UTF-8").encode(charBuffer
);
104 byte[] bytes
= Arrays
.copyOfRange(byteBuffer
.array(), byteBuffer
.position(), byteBuffer
.limit());
105 Arrays
.fill(charBuffer
.array(), '\u0000'); // clear sensitive data
106 Arrays
.fill(byteBuffer
.array(), (byte) 0); // clear sensitive data
110 private char[] toChars(Object obj
) {
111 if (obj
instanceof char[])
113 if (!(obj
instanceof byte[]))
114 throw new IllegalArgumentException(obj
.getClass() + " is not a byte array");
115 ByteBuffer fromBuffer
= ByteBuffer
.wrap((byte[]) obj
);
116 CharBuffer toBuffer
= Charset
.forName("UTF-8").decode(fromBuffer
);
117 char[] res
= Arrays
.copyOfRange(toBuffer
.array(), toBuffer
.position(), toBuffer
.limit());
118 Arrays
.fill(fromBuffer
.array(), (byte) 0); // clear sensitive data
119 Arrays
.fill((byte[]) obj
, (byte) 0); // clear sensitive data
120 Arrays
.fill(toBuffer
.array(), '\u0000'); // clear sensitive data
125 public LdapName
getDn() {
130 public synchronized Attributes
getAttributes() {
131 return isEditing() ?
getModifiedAttributes() : publishedAttributes
;
134 /** Should only be called from working copy thread. */
135 private synchronized Attributes
getModifiedAttributes() {
136 assert getWc() != null;
137 return getWc().getAttributes(getDn());
140 protected synchronized boolean isEditing() {
141 return getWc() != null && getModifiedAttributes() != null;
144 private synchronized UserDirectoryWorkingCopy
getWc() {
145 return userAdmin
.getWorkingCopy();
148 protected synchronized void startEditing() {
150 throw new UserDirectoryException("Cannot edit frozen view");
151 if (getUserAdmin().isReadOnly())
152 throw new UserDirectoryException("User directory is read-only");
153 assert getModifiedAttributes() == null;
154 getWc().startEditing(this);
155 // modifiedAttributes = (Attributes) publishedAttributes.clone();
158 public synchronized void publishAttributes(Attributes modifiedAttributes
) {
159 publishedAttributes
= modifiedAttributes
;
162 public DirectoryUser
getPublished() {
163 return new LdifUser(userAdmin
, dn
, publishedAttributes
, true);
167 public int hashCode() {
168 return dn
.hashCode();
172 public boolean equals(Object obj
) {
175 if (obj
instanceof LdifUser
) {
176 LdifUser that
= (LdifUser
) obj
;
177 return this.dn
.equals(that
.dn
);
183 public String
toString() {
184 return dn
.toString();
187 protected AbstractUserDirectory
getUserAdmin() {
191 private class AttributeDictionary
extends Dictionary
<String
, Object
> {
192 private final List
<String
> effectiveKeys
= new ArrayList
<String
>();
193 private final List
<String
> attrFilter
;
194 private final Boolean includeFilter
;
196 public AttributeDictionary(Boolean includeFilter
) {
197 this.attrFilter
= userAdmin
.getCredentialAttributeIds();
198 this.includeFilter
= includeFilter
;
200 NamingEnumeration
<String
> ids
= getAttributes().getIDs();
201 while (ids
.hasMore()) {
202 String id
= ids
.next();
203 if (includeFilter
&& attrFilter
.contains(id
))
204 effectiveKeys
.add(id
);
205 else if (!includeFilter
&& !attrFilter
.contains(id
))
206 effectiveKeys
.add(id
);
208 } catch (NamingException e
) {
209 throw new UserDirectoryException("Cannot initialise attribute dictionary", e
);
215 return effectiveKeys
.size();
219 public boolean isEmpty() {
220 return effectiveKeys
.size() == 0;
224 public Enumeration
<String
> keys() {
225 return Collections
.enumeration(effectiveKeys
);
229 public Enumeration
<Object
> elements() {
230 final Iterator
<String
> it
= effectiveKeys
.iterator();
231 return new Enumeration
<Object
>() {
234 public boolean hasMoreElements() {
239 public Object
nextElement() {
240 String key
= it
.next();
248 public Object
get(Object key
) {
250 Attribute attr
= getAttributes().get(key
.toString());
253 Object value
= attr
.get();
254 if (value
instanceof byte[]) {
255 if (key
.equals(LdapAttrs
.userPassword
.name()))
256 // TODO other cases (certificates, images)
258 value
= new String((byte[]) value
, Charset
.forName("UTF-8"));
260 if (attr
.size() == 1)
262 if (!attr
.getID().equals(LdapAttrs
.objectClass
.name()))
264 // special case for object class
265 NamingEnumeration
<?
> en
= attr
.getAll();
266 Set
<String
> objectClasses
= new HashSet
<String
>();
267 while (en
.hasMore()) {
268 String objectClass
= en
.next().toString();
269 objectClasses
.add(objectClass
);
272 if (objectClasses
.contains(userAdmin
.getUserObjectClass()))
273 return userAdmin
.getUserObjectClass();
274 else if (objectClasses
.contains(userAdmin
.getGroupObjectClass()))
275 return userAdmin
.getGroupObjectClass();
278 } catch (NamingException e
) {
279 throw new UserDirectoryException("Cannot get value for attribute " + key
, e
);
284 public Object
put(String key
, Object value
) {
286 // TODO persist to other sources (like PKCS12)
287 char[] password
= toChars(value
);
288 byte[] hashedPassword
= hash(password
);
289 return put(LdapAttrs
.userPassword
.name(), hashedPassword
);
292 userAdmin
.checkEdit();
296 if (!(value
instanceof String
|| value
instanceof byte[]))
297 throw new IllegalArgumentException("Value must be String or byte[]");
299 if (includeFilter
&& !attrFilter
.contains(key
))
300 throw new IllegalArgumentException("Key " + key
+ " not included");
301 else if (!includeFilter
&& attrFilter
.contains(key
))
302 throw new IllegalArgumentException("Key " + key
+ " excluded");
305 Attribute attribute
= getModifiedAttributes().get(key
.toString());
306 attribute
= new BasicAttribute(key
.toString());
307 if (value
instanceof String
&& !isAsciiPrintable(((String
) value
)))
309 attribute
.add(((String
) value
).getBytes("UTF-8"));
310 } catch (UnsupportedEncodingException e
) {
311 throw new UserDirectoryException("Cannot encode " + value
, e
);
314 attribute
.add(value
);
315 Attribute previousAttribute
= getModifiedAttributes().put(attribute
);
316 if (previousAttribute
!= null)
317 return previousAttribute
.get();
320 } catch (NamingException e
) {
321 throw new UserDirectoryException("Cannot get value for attribute " + key
, e
);
326 public Object
remove(Object key
) {
327 userAdmin
.checkEdit();
331 if (includeFilter
&& !attrFilter
.contains(key
))
332 throw new IllegalArgumentException("Key " + key
+ " not included");
333 else if (!includeFilter
&& attrFilter
.contains(key
))
334 throw new IllegalArgumentException("Key " + key
+ " excluded");
337 Attribute attr
= getModifiedAttributes().remove(key
.toString());
342 } catch (NamingException e
) {
343 throw new UserDirectoryException("Cannot remove attribute " + key
, e
);
348 private static boolean isAsciiPrintable(String str
) {
352 int sz
= str
.length();
353 for (int i
= 0; i
< sz
; i
++) {
354 if (isAsciiPrintable(str
.charAt(i
)) == false) {
361 private static boolean isAsciiPrintable(char ch
) {
362 return ch
>= 32 && ch
< 127;