]> git.argeo.org Git - lgpl/argeo-commons.git/blob - LdifUser.java
d5ddba50c17060e7d29db08aa2cb38dc5bdabfb7
[lgpl/argeo-commons.git] / LdifUser.java
1 package org.argeo.osgi.useradmin;
2
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.Collections;
10 import java.util.Dictionary;
11 import java.util.Enumeration;
12 import java.util.HashSet;
13 import java.util.Iterator;
14 import java.util.List;
15 import java.util.Set;
16
17 import javax.naming.NamingEnumeration;
18 import javax.naming.NamingException;
19 import javax.naming.directory.Attribute;
20 import javax.naming.directory.Attributes;
21 import javax.naming.directory.BasicAttribute;
22 import javax.naming.ldap.LdapName;
23
24 import org.apache.commons.codec.binary.Base64;
25 import org.apache.commons.codec.digest.DigestUtils;
26
27 /** Directory user implementation */
28 class LdifUser implements DirectoryUser {
29 private final AbstractUserDirectory userAdmin;
30
31 private final LdapName dn;
32
33 private final boolean frozen;
34 private Attributes publishedAttributes;
35
36 private final AttributeDictionary properties;
37 private final AttributeDictionary credentials;
38
39 LdifUser(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes) {
40 this(userAdmin, dn, attributes, false);
41 }
42
43 private LdifUser(AbstractUserDirectory userAdmin, LdapName dn,
44 Attributes attributes, boolean frozen) {
45 this.userAdmin = userAdmin;
46 this.dn = dn;
47 this.publishedAttributes = attributes;
48 properties = new AttributeDictionary(false);
49 credentials = new AttributeDictionary(true);
50 this.frozen = frozen;
51 }
52
53 @Override
54 public String getName() {
55 return dn.toString();
56 }
57
58 @Override
59 public int getType() {
60 return USER;
61 }
62
63 @Override
64 public Dictionary<String, Object> getProperties() {
65 return properties;
66 }
67
68 @Override
69 public Dictionary<String, Object> getCredentials() {
70 return credentials;
71 }
72
73 @Override
74 public boolean hasCredential(String key, Object value) {
75 if (key == null) {
76 // TODO check other sources (like PKCS12)
77 char[] password = toChars(value);
78 byte[] hashedPassword = hash(password);
79 return hasCredential(LdifName.userPassword.name(), hashedPassword);
80 }
81
82 Object storedValue = getCredentials().get(key);
83 if (storedValue == null || value == null)
84 return false;
85 if (!(value instanceof String || value instanceof byte[]))
86 return false;
87 if (storedValue instanceof String && value instanceof String)
88 return storedValue.equals(value);
89 if (storedValue instanceof byte[] && value instanceof byte[])
90 return Arrays.equals((byte[]) storedValue, (byte[]) value);
91 return false;
92 }
93
94 /** Hash and clear the password */
95 private byte[] hash(char[] password) {
96 byte[] hashedPassword = ("{SHA}" + Base64
97 .encodeBase64String(DigestUtils.sha1(toBytes(password))))
98 .getBytes();
99 Arrays.fill(password, '\u0000');
100 return hashedPassword;
101 }
102
103 private byte[] toBytes(char[] chars) {
104 CharBuffer charBuffer = CharBuffer.wrap(chars);
105 ByteBuffer byteBuffer = Charset.forName("UTF-8").encode(charBuffer);
106 byte[] bytes = Arrays.copyOfRange(byteBuffer.array(),
107 byteBuffer.position(), byteBuffer.limit());
108 Arrays.fill(charBuffer.array(), '\u0000'); // clear sensitive data
109 Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data
110 return bytes;
111 }
112
113 private char[] toChars(Object obj) {
114 if (obj instanceof char[])
115 return (char[]) obj;
116 if (!(obj instanceof byte[]))
117 throw new IllegalArgumentException(obj.getClass()
118 + " is not a byte array");
119 ByteBuffer fromBuffer = ByteBuffer.wrap((byte[]) obj);
120 CharBuffer toBuffer = Charset.forName("UTF-8").decode(fromBuffer);
121 char[] res = Arrays.copyOfRange(toBuffer.array(), toBuffer.position(),
122 toBuffer.limit());
123 Arrays.fill(fromBuffer.array(), (byte) 0); // clear sensitive data
124 Arrays.fill((byte[]) obj, (byte) 0); // clear sensitive data
125 Arrays.fill(toBuffer.array(), '\u0000'); // clear sensitive data
126 return res;
127 }
128
129 @Override
130 public LdapName getDn() {
131 return dn;
132 }
133
134 @Override
135 public synchronized Attributes getAttributes() {
136 return isEditing() ? getModifiedAttributes() : publishedAttributes;
137 }
138
139 /** Should only be called from working copy thread. */
140 private synchronized Attributes getModifiedAttributes() {
141 assert getWc() != null;
142 return getWc().getAttributes(getDn());
143 }
144
145 protected synchronized boolean isEditing() {
146 return getWc() != null && getModifiedAttributes() != null;
147 }
148
149 private synchronized UserDirectoryWorkingCopy getWc() {
150 return userAdmin.getWorkingCopy();
151 }
152
153 protected synchronized void startEditing() {
154 if (frozen)
155 throw new UserDirectoryException("Cannot edit frozen view");
156 if (getUserAdmin().isReadOnly())
157 throw new UserDirectoryException("User directory is read-only");
158 assert getModifiedAttributes() == null;
159 getWc().startEditing(this);
160 // modifiedAttributes = (Attributes) publishedAttributes.clone();
161 }
162
163 public synchronized void publishAttributes(Attributes modifiedAttributes) {
164 publishedAttributes = modifiedAttributes;
165 }
166
167 public DirectoryUser getPublished() {
168 return new LdifUser(userAdmin, dn, publishedAttributes, true);
169 }
170
171 @Override
172 public int hashCode() {
173 return dn.hashCode();
174 }
175
176 @Override
177 public boolean equals(Object obj) {
178 if (this == obj)
179 return true;
180 if (obj instanceof LdifUser) {
181 LdifUser that = (LdifUser) obj;
182 return this.dn.equals(that.dn);
183 }
184 return false;
185 }
186
187 @Override
188 public String toString() {
189 return dn.toString();
190 }
191
192 protected AbstractUserDirectory getUserAdmin() {
193 return userAdmin;
194 }
195
196 private class AttributeDictionary extends Dictionary<String, Object> {
197 private final List<String> effectiveKeys = new ArrayList<String>();
198 private final List<String> attrFilter;
199 private final Boolean includeFilter;
200
201 public AttributeDictionary(Boolean includeFilter) {
202 this.attrFilter = userAdmin.getCredentialAttributeIds();
203 this.includeFilter = includeFilter;
204 try {
205 NamingEnumeration<String> ids = getAttributes().getIDs();
206 while (ids.hasMore()) {
207 String id = ids.next();
208 if (includeFilter && attrFilter.contains(id))
209 effectiveKeys.add(id);
210 else if (!includeFilter && !attrFilter.contains(id))
211 effectiveKeys.add(id);
212 }
213 } catch (NamingException e) {
214 throw new UserDirectoryException(
215 "Cannot initialise attribute dictionary", e);
216 }
217 }
218
219 @Override
220 public int size() {
221 return effectiveKeys.size();
222 }
223
224 @Override
225 public boolean isEmpty() {
226 return effectiveKeys.size() == 0;
227 }
228
229 @Override
230 public Enumeration<String> keys() {
231 return Collections.enumeration(effectiveKeys);
232 }
233
234 @Override
235 public Enumeration<Object> elements() {
236 final Iterator<String> it = effectiveKeys.iterator();
237 return new Enumeration<Object>() {
238
239 @Override
240 public boolean hasMoreElements() {
241 return it.hasNext();
242 }
243
244 @Override
245 public Object nextElement() {
246 String key = it.next();
247 return get(key);
248 }
249
250 };
251 }
252
253 @Override
254 public Object get(Object key) {
255 try {
256 Attribute attr = getAttributes().get(key.toString());
257 if (attr == null)
258 return null;
259 Object value = attr.get();
260 if (value instanceof byte[]) {
261 if (key.equals(LdifName.userPassword.name()))
262 // TODO other cases (certificates, images)
263 return value;
264 value = new String((byte[]) value, Charset.forName("UTF-8"));
265 }
266 if (attr.size() == 1)
267 return value;
268 if (!attr.getID().equals(LdifName.objectClass.name()))
269 return value;
270 // special case for object class
271 NamingEnumeration<?> en = attr.getAll();
272 Set<String> objectClasses = new HashSet<String>();
273 while (en.hasMore()) {
274 String objectClass = en.next().toString();
275 objectClasses.add(objectClass);
276 }
277
278 if (objectClasses.contains(userAdmin.getUserObjectClass()))
279 return userAdmin.getUserObjectClass();
280 else if (objectClasses
281 .contains(userAdmin.getGroupObjectClass()))
282 return userAdmin.getGroupObjectClass();
283 else
284 return value;
285 } catch (NamingException e) {
286 throw new UserDirectoryException(
287 "Cannot get value for attribute " + key, e);
288 }
289 }
290
291 @Override
292 public Object put(String key, Object value) {
293 if (key == null) {
294 // TODO persist to other sources (like PKCS12)
295 char[] password = toChars(value);
296 byte[] hashedPassword = hash(password);
297 return put(LdifName.userPassword.name(), hashedPassword);
298 }
299
300 userAdmin.checkEdit();
301 if (!isEditing())
302 startEditing();
303
304 if (!(value instanceof String || value instanceof byte[]))
305 throw new IllegalArgumentException(
306 "Value must be String or byte[]");
307
308 if (includeFilter && !attrFilter.contains(key))
309 throw new IllegalArgumentException("Key " + key
310 + " not included");
311 else if (!includeFilter && attrFilter.contains(key))
312 throw new IllegalArgumentException("Key " + key + " excluded");
313
314 try {
315 Attribute attribute = getModifiedAttributes().get(
316 key.toString());
317 attribute = new BasicAttribute(key.toString());
318 if (value instanceof String
319 && !isAsciiPrintable(((String) value)))
320 try {
321 attribute.add(((String) value).getBytes("UTF-8"));
322 } catch (UnsupportedEncodingException e) {
323 throw new UserDirectoryException("Cannot encode "
324 + value, e);
325 }
326 else
327 attribute.add(value);
328 Attribute previousAttribute = getModifiedAttributes().put(
329 attribute);
330 if (previousAttribute != null)
331 return previousAttribute.get();
332 else
333 return null;
334 } catch (NamingException e) {
335 throw new UserDirectoryException(
336 "Cannot get value for attribute " + key, e);
337 }
338 }
339
340 @Override
341 public Object remove(Object key) {
342 userAdmin.checkEdit();
343 if (!isEditing())
344 startEditing();
345
346 if (includeFilter && !attrFilter.contains(key))
347 throw new IllegalArgumentException("Key " + key
348 + " not included");
349 else if (!includeFilter && attrFilter.contains(key))
350 throw new IllegalArgumentException("Key " + key + " excluded");
351
352 try {
353 Attribute attr = getModifiedAttributes().remove(key.toString());
354 if (attr != null)
355 return attr.get();
356 else
357 return null;
358 } catch (NamingException e) {
359 throw new UserDirectoryException("Cannot remove attribute "
360 + key, e);
361 }
362 }
363 }
364
365 private static boolean isAsciiPrintable(String str) {
366 if (str == null) {
367 return false;
368 }
369 int sz = str.length();
370 for (int i = 0; i < sz; i++) {
371 if (isAsciiPrintable(str.charAt(i)) == false) {
372 return false;
373 }
374 }
375 return true;
376 }
377
378 private static boolean isAsciiPrintable(char ch) {
379 return ch >= 32 && ch < 127;
380 }
381
382 }