]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java
Factorize 'node' URI scheme constant.
[lgpl/argeo-commons.git] / org.argeo.enterprise / src / org / argeo / osgi / useradmin / 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.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;
16 import java.util.Set;
17
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;
24
25 import org.argeo.naming.LdapAttrs;
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, Attributes attributes, boolean frozen) {
44 this.userAdmin = userAdmin;
45 this.dn = dn;
46 this.publishedAttributes = attributes;
47 properties = new AttributeDictionary(false);
48 credentials = new AttributeDictionary(true);
49 this.frozen = frozen;
50 }
51
52 @Override
53 public String getName() {
54 return dn.toString();
55 }
56
57 @Override
58 public int getType() {
59 return USER;
60 }
61
62 @Override
63 public Dictionary<String, Object> getProperties() {
64 return properties;
65 }
66
67 @Override
68 public Dictionary<String, Object> getCredentials() {
69 return credentials;
70 }
71
72 @Override
73 public boolean hasCredential(String key, Object value) {
74 if (key == null) {
75 // TODO check other sources (like PKCS12)
76 char[] password = toChars(value);
77 byte[] hashedPassword = hash(password);
78 return hasCredential(LdapAttrs.userPassword.name(), hashedPassword);
79 }
80
81 Object storedValue = getCredentials().get(key);
82 if (storedValue == null || value == null)
83 return false;
84 if (!(value instanceof String || value instanceof byte[]))
85 return false;
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);
90 return false;
91 }
92
93 /** Hash and clear the password */
94 private byte[] hash(char[] password) {
95 byte[] hashedPassword = ("{SHA}" + Base64.getEncoder().encodeToString(DigestUtils.sha1(toBytes(password))))
96 .getBytes();
97 Arrays.fill(password, '\u0000');
98 return hashedPassword;
99 }
100
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
107 return bytes;
108 }
109
110 private char[] toChars(Object obj) {
111 if (obj instanceof char[])
112 return (char[]) obj;
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
121 return res;
122 }
123
124 @Override
125 public LdapName getDn() {
126 return dn;
127 }
128
129 @Override
130 public synchronized Attributes getAttributes() {
131 return isEditing() ? getModifiedAttributes() : publishedAttributes;
132 }
133
134 /** Should only be called from working copy thread. */
135 private synchronized Attributes getModifiedAttributes() {
136 assert getWc() != null;
137 return getWc().getAttributes(getDn());
138 }
139
140 protected synchronized boolean isEditing() {
141 return getWc() != null && getModifiedAttributes() != null;
142 }
143
144 private synchronized UserDirectoryWorkingCopy getWc() {
145 return userAdmin.getWorkingCopy();
146 }
147
148 protected synchronized void startEditing() {
149 if (frozen)
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();
156 }
157
158 public synchronized void publishAttributes(Attributes modifiedAttributes) {
159 publishedAttributes = modifiedAttributes;
160 }
161
162 public DirectoryUser getPublished() {
163 return new LdifUser(userAdmin, dn, publishedAttributes, true);
164 }
165
166 @Override
167 public int hashCode() {
168 return dn.hashCode();
169 }
170
171 @Override
172 public boolean equals(Object obj) {
173 if (this == obj)
174 return true;
175 if (obj instanceof LdifUser) {
176 LdifUser that = (LdifUser) obj;
177 return this.dn.equals(that.dn);
178 }
179 return false;
180 }
181
182 @Override
183 public String toString() {
184 return dn.toString();
185 }
186
187 protected AbstractUserDirectory getUserAdmin() {
188 return userAdmin;
189 }
190
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;
195
196 public AttributeDictionary(Boolean includeFilter) {
197 this.attrFilter = userAdmin.getCredentialAttributeIds();
198 this.includeFilter = includeFilter;
199 try {
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);
207 }
208 } catch (NamingException e) {
209 throw new UserDirectoryException("Cannot initialise attribute dictionary", e);
210 }
211 }
212
213 @Override
214 public int size() {
215 return effectiveKeys.size();
216 }
217
218 @Override
219 public boolean isEmpty() {
220 return effectiveKeys.size() == 0;
221 }
222
223 @Override
224 public Enumeration<String> keys() {
225 return Collections.enumeration(effectiveKeys);
226 }
227
228 @Override
229 public Enumeration<Object> elements() {
230 final Iterator<String> it = effectiveKeys.iterator();
231 return new Enumeration<Object>() {
232
233 @Override
234 public boolean hasMoreElements() {
235 return it.hasNext();
236 }
237
238 @Override
239 public Object nextElement() {
240 String key = it.next();
241 return get(key);
242 }
243
244 };
245 }
246
247 @Override
248 public Object get(Object key) {
249 try {
250 Attribute attr = getAttributes().get(key.toString());
251 if (attr == null)
252 return null;
253 Object value = attr.get();
254 if (value instanceof byte[]) {
255 if (key.equals(LdapAttrs.userPassword.name()))
256 // TODO other cases (certificates, images)
257 return value;
258 value = new String((byte[]) value, Charset.forName("UTF-8"));
259 }
260 if (attr.size() == 1)
261 return value;
262 if (!attr.getID().equals(LdapAttrs.objectClass.name()))
263 return value;
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);
270 }
271
272 if (objectClasses.contains(userAdmin.getUserObjectClass()))
273 return userAdmin.getUserObjectClass();
274 else if (objectClasses.contains(userAdmin.getGroupObjectClass()))
275 return userAdmin.getGroupObjectClass();
276 else
277 return value;
278 } catch (NamingException e) {
279 throw new UserDirectoryException("Cannot get value for attribute " + key, e);
280 }
281 }
282
283 @Override
284 public Object put(String key, Object value) {
285 if (key == null) {
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);
290 }
291
292 userAdmin.checkEdit();
293 if (!isEditing())
294 startEditing();
295
296 if (!(value instanceof String || value instanceof byte[]))
297 throw new IllegalArgumentException("Value must be String or byte[]");
298
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");
303
304 try {
305 Attribute attribute = getModifiedAttributes().get(key.toString());
306 attribute = new BasicAttribute(key.toString());
307 if (value instanceof String && !isAsciiPrintable(((String) value)))
308 try {
309 attribute.add(((String) value).getBytes("UTF-8"));
310 } catch (UnsupportedEncodingException e) {
311 throw new UserDirectoryException("Cannot encode " + value, e);
312 }
313 else
314 attribute.add(value);
315 Attribute previousAttribute = getModifiedAttributes().put(attribute);
316 if (previousAttribute != null)
317 return previousAttribute.get();
318 else
319 return null;
320 } catch (NamingException e) {
321 throw new UserDirectoryException("Cannot get value for attribute " + key, e);
322 }
323 }
324
325 @Override
326 public Object remove(Object key) {
327 userAdmin.checkEdit();
328 if (!isEditing())
329 startEditing();
330
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");
335
336 try {
337 Attribute attr = getModifiedAttributes().remove(key.toString());
338 if (attr != null)
339 return attr.get();
340 else
341 return null;
342 } catch (NamingException e) {
343 throw new UserDirectoryException("Cannot remove attribute " + key, e);
344 }
345 }
346 }
347
348 private static boolean isAsciiPrintable(String str) {
349 if (str == null) {
350 return false;
351 }
352 int sz = str.length();
353 for (int i = 0; i < sz; i++) {
354 if (isAsciiPrintable(str.charAt(i)) == false) {
355 return false;
356 }
357 }
358 return true;
359 }
360
361 private static boolean isAsciiPrintable(char ch) {
362 return ch >= 32 && ch < 127;
363 }
364
365 }