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