]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.util/src/org/argeo/util/directory/ldap/DefaultLdapEntry.java
8eff6690003c014a70b947fb1d19b8e42b71ca53
[lgpl/argeo-commons.git] / org.argeo.util / src / org / argeo / util / directory / ldap / DefaultLdapEntry.java
1 package org.argeo.util.directory.ldap;
2
3 import static java.nio.charset.StandardCharsets.US_ASCII;
4
5 import java.math.BigInteger;
6 import java.nio.charset.StandardCharsets;
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.Iterator;
14 import java.util.List;
15 import java.util.Objects;
16 import java.util.StringJoiner;
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.util.directory.DirectoryDigestUtils;
26 import org.argeo.util.naming.LdapAttrs;
27 import org.argeo.util.naming.LdapObjs;
28 import org.argeo.util.naming.SharedSecret;
29
30 /** An entry in an LDAP (or LDIF) directory. */
31 public class DefaultLdapEntry implements LdapEntry {
32 private final AbstractLdapDirectory directory;
33
34 private final LdapName dn;
35
36 private Attributes publishedAttributes;
37
38 // Temporarily expose the fields
39 protected final AttributeDictionary properties;
40 protected final AttributeDictionary credentials;
41
42 protected DefaultLdapEntry(AbstractLdapDirectory directory, LdapName dn, Attributes attributes) {
43 Objects.requireNonNull(directory);
44 Objects.requireNonNull(dn);
45 this.directory = directory;
46 this.dn = dn;
47 this.publishedAttributes = attributes;
48 properties = new AttributeDictionary(false);
49 credentials = new AttributeDictionary(true);
50 }
51
52 @Override
53 public LdapName getDn() {
54 return dn;
55 }
56
57 public synchronized Attributes getAttributes() {
58 return isEditing() ? getModifiedAttributes() : publishedAttributes;
59 }
60
61 @Override
62 public List<LdapName> getReferences(String attributeId) {
63 Attribute memberAttribute = getAttributes().get(attributeId);
64 if (memberAttribute == null)
65 return new ArrayList<LdapName>();
66 try {
67 List<LdapName> roles = new ArrayList<LdapName>();
68 NamingEnumeration<?> values = memberAttribute.getAll();
69 while (values.hasMore()) {
70 LdapName dn = new LdapName(values.next().toString());
71 roles.add(dn);
72 }
73 return roles;
74 } catch (NamingException e) {
75 throw new IllegalStateException("Cannot get members", e);
76 }
77
78 }
79
80 /** Should only be called from working copy thread. */
81 protected synchronized Attributes getModifiedAttributes() {
82 assert getWc() != null;
83 return getWc().getModifiedData().get(getDn());
84 }
85
86 protected synchronized boolean isEditing() {
87 return getWc() != null && getModifiedAttributes() != null;
88 }
89
90 private synchronized LdapEntryWorkingCopy getWc() {
91 return directory.getWorkingCopy();
92 }
93
94 protected synchronized void startEditing() {
95 // if (frozen)
96 // throw new IllegalStateException("Cannot edit frozen view");
97 if (directory.isReadOnly())
98 throw new IllegalStateException("User directory is read-only");
99 assert getModifiedAttributes() == null;
100 getWc().startEditing(this);
101 // modifiedAttributes = (Attributes) publishedAttributes.clone();
102 }
103
104 public synchronized void publishAttributes(Attributes modifiedAttributes) {
105 publishedAttributes = modifiedAttributes;
106 }
107
108 /*
109 * PROPERTIES
110 */
111 @Override
112 public Dictionary<String, Object> getProperties() {
113 return properties;
114 }
115
116 /*
117 * CREDENTIALS
118 */
119 @Override
120 public boolean hasCredential(String key, Object value) {
121 if (key == null) {
122 // TODO check other sources (like PKCS12)
123 // String pwd = new String((char[]) value);
124 // authPassword (RFC 312 https://tools.ietf.org/html/rfc3112)
125 char[] password = DirectoryDigestUtils.bytesToChars(value);
126
127 if (getDirectory().getForcedPassword() != null
128 && getDirectory().getForcedPassword().equals(new String(password)))
129 return true;
130
131 AuthPassword authPassword = AuthPassword.matchAuthValue(getAttributes(), password);
132 if (authPassword != null) {
133 if (authPassword.getAuthScheme().equals(SharedSecret.X_SHARED_SECRET)) {
134 SharedSecret onceToken = new SharedSecret(authPassword);
135 if (onceToken.isExpired()) {
136 // AuthPassword.remove(getAttributes(), onceToken);
137 return false;
138 } else {
139 // boolean wasRemoved = AuthPassword.remove(getAttributes(), onceToken);
140 return true;
141 }
142 // TODO delete expired tokens?
143 } else {
144 // TODO implement SHA
145 throw new UnsupportedOperationException(
146 "Unsupported authPassword scheme " + authPassword.getAuthScheme());
147 }
148 }
149
150 // Regular password
151 // byte[] hashedPassword = hash(password, DigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256);
152 if (hasCredential(LdapAttrs.userPassword.name(), DirectoryDigestUtils.charsToBytes(password)))
153 return true;
154 return false;
155 }
156
157 Object storedValue = credentials.get(key);
158 if (storedValue == null || value == null)
159 return false;
160 if (!(value instanceof String || value instanceof byte[]))
161 return false;
162 if (storedValue instanceof String && value instanceof String)
163 return storedValue.equals(value);
164 if (storedValue instanceof byte[] && value instanceof byte[]) {
165 String storedBase64 = new String((byte[]) storedValue, US_ASCII);
166 String passwordScheme = null;
167 if (storedBase64.charAt(0) == '{') {
168 int index = storedBase64.indexOf('}');
169 if (index > 0) {
170 passwordScheme = storedBase64.substring(1, index);
171 String storedValueBase64 = storedBase64.substring(index + 1);
172 byte[] storedValueBytes = Base64.getDecoder().decode(storedValueBase64);
173 char[] passwordValue = DirectoryDigestUtils.bytesToChars((byte[]) value);
174 byte[] valueBytes;
175 if (DirectoryDigestUtils.PASSWORD_SCHEME_SHA.equals(passwordScheme)) {
176 valueBytes = DirectoryDigestUtils.toPasswordScheme(passwordScheme, passwordValue, null, null,
177 null);
178 } else if (DirectoryDigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256.equals(passwordScheme)) {
179 // see https://www.thesubtlety.com/post/a-389-ds-pbkdf2-password-checker/
180 byte[] iterationsArr = Arrays.copyOfRange(storedValueBytes, 0, 4);
181 BigInteger iterations = new BigInteger(iterationsArr);
182 byte[] salt = Arrays.copyOfRange(storedValueBytes, iterationsArr.length,
183 iterationsArr.length + 64);
184 byte[] keyArr = Arrays.copyOfRange(storedValueBytes, iterationsArr.length + salt.length,
185 storedValueBytes.length);
186 int keyLengthBits = keyArr.length * 8;
187 valueBytes = DirectoryDigestUtils.toPasswordScheme(passwordScheme, passwordValue, salt,
188 iterations.intValue(), keyLengthBits);
189 } else {
190 throw new UnsupportedOperationException("Unknown password scheme " + passwordScheme);
191 }
192 return Arrays.equals(storedValueBytes, valueBytes);
193 }
194 }
195 }
196 // if (storedValue instanceof byte[] && value instanceof byte[]) {
197 // return Arrays.equals((byte[]) storedValue, (byte[]) value);
198 // }
199 return false;
200 }
201
202 /** Hash the password */
203 private static byte[] sha1hash(char[] password) {
204 byte[] hashedPassword = ("{SHA}" + Base64.getEncoder()
205 .encodeToString(DirectoryDigestUtils.sha1(DirectoryDigestUtils.charsToBytes(password))))
206 .getBytes(StandardCharsets.UTF_8);
207 return hashedPassword;
208 }
209
210 public AbstractLdapDirectory getDirectory() {
211 return directory;
212 }
213
214 public LdapDirectoryDao getDirectoryDao() {
215 return directory.getDirectoryDao();
216 }
217
218 @Override
219 public int hashCode() {
220 return dn.hashCode();
221 }
222
223 @Override
224 public boolean equals(Object obj) {
225 if (this == obj)
226 return true;
227 if (obj instanceof LdapEntry) {
228 LdapEntry that = (LdapEntry) obj;
229 return this.dn.equals(that.getDn());
230 }
231 return false;
232 }
233
234 @Override
235 public String toString() {
236 return dn.toString();
237 }
238
239 private static boolean isAsciiPrintable(String str) {
240 if (str == null) {
241 return false;
242 }
243 int sz = str.length();
244 for (int i = 0; i < sz; i++) {
245 if (isAsciiPrintable(str.charAt(i)) == false) {
246 return false;
247 }
248 }
249 return true;
250 }
251
252 private static boolean isAsciiPrintable(char ch) {
253 return ch >= 32 && ch < 127;
254 }
255
256 protected class AttributeDictionary extends Dictionary<String, Object> {
257 private final List<String> effectiveKeys = new ArrayList<String>();
258 private final List<String> attrFilter;
259 private final Boolean includeFilter;
260
261 public AttributeDictionary(Boolean credentials) {
262 this.attrFilter = getDirectory().getCredentialAttributeIds();
263 this.includeFilter = credentials;
264 try {
265 NamingEnumeration<String> ids = getAttributes().getIDs();
266 while (ids.hasMore()) {
267 String id = ids.next();
268 if (credentials && attrFilter.contains(id))
269 effectiveKeys.add(id);
270 else if (!credentials && !attrFilter.contains(id))
271 effectiveKeys.add(id);
272 }
273 } catch (NamingException e) {
274 throw new IllegalStateException("Cannot initialise attribute dictionary", e);
275 }
276 if (!credentials)
277 effectiveKeys.add(LdapAttrs.objectClasses.name());
278 }
279
280 @Override
281 public int size() {
282 return effectiveKeys.size();
283 }
284
285 @Override
286 public boolean isEmpty() {
287 return effectiveKeys.size() == 0;
288 }
289
290 @Override
291 public Enumeration<String> keys() {
292 return Collections.enumeration(effectiveKeys);
293 }
294
295 @Override
296 public Enumeration<Object> elements() {
297 final Iterator<String> it = effectiveKeys.iterator();
298 return new Enumeration<Object>() {
299
300 @Override
301 public boolean hasMoreElements() {
302 return it.hasNext();
303 }
304
305 @Override
306 public Object nextElement() {
307 String key = it.next();
308 return get(key);
309 }
310
311 };
312 }
313
314 @Override
315 public Object get(Object key) {
316 try {
317 Attribute attr = !key.equals(LdapAttrs.objectClasses.name()) ? getAttributes().get(key.toString())
318 : getAttributes().get(LdapAttrs.objectClass.name());
319 if (attr == null)
320 return null;
321 Object value = attr.get();
322 if (value instanceof byte[]) {
323 if (key.equals(LdapAttrs.userPassword.name()))
324 // TODO other cases (certificates, images)
325 return value;
326 value = new String((byte[]) value, StandardCharsets.UTF_8);
327 }
328 if (attr.size() == 1)
329 return value;
330 // special case for object class
331 if (key.equals(LdapAttrs.objectClass.name())) {
332 // TODO support multiple object classes
333 NamingEnumeration<?> en = attr.getAll();
334 String first = null;
335 attrs: while (en.hasMore()) {
336 String v = en.next().toString();
337 if (v.equalsIgnoreCase(LdapObjs.top.name()))
338 continue attrs;
339 if (first == null)
340 first = v;
341 if (v.equalsIgnoreCase(getDirectory().getUserObjectClass()))
342 return getDirectory().getUserObjectClass();
343 else if (v.equalsIgnoreCase(getDirectory().getGroupObjectClass()))
344 return getDirectory().getGroupObjectClass();
345 }
346 if (first != null)
347 return first;
348 throw new IllegalStateException("Cannot find objectClass in " + value);
349 } else {
350 NamingEnumeration<?> en = attr.getAll();
351 StringJoiner values = new StringJoiner("\n");
352 while (en.hasMore()) {
353 String v = en.next().toString();
354 values.add(v);
355 }
356 return values.toString();
357 }
358 // else
359 // return value;
360 } catch (NamingException e) {
361 throw new IllegalStateException("Cannot get value for attribute " + key, e);
362 }
363 }
364
365 @Override
366 public Object put(String key, Object value) {
367 if (key == null) {
368 // TODO persist to other sources (like PKCS12)
369 char[] password = DirectoryDigestUtils.bytesToChars(value);
370 byte[] hashedPassword = sha1hash(password);
371 return put(LdapAttrs.userPassword.name(), hashedPassword);
372 }
373 if (key.startsWith("X-")) {
374 return put(LdapAttrs.authPassword.name(), value);
375 }
376
377 getDirectory().checkEdit();
378 if (!isEditing())
379 startEditing();
380
381 if (!(value instanceof String || value instanceof byte[]))
382 throw new IllegalArgumentException("Value must be String or byte[]");
383
384 if (includeFilter && !attrFilter.contains(key))
385 throw new IllegalArgumentException("Key " + key + " not included");
386 else if (!includeFilter && attrFilter.contains(key))
387 throw new IllegalArgumentException("Key " + key + " excluded");
388
389 try {
390 Attribute attribute = getModifiedAttributes().get(key.toString());
391 // if (attribute == null) // block unit tests
392 attribute = new BasicAttribute(key.toString());
393 if (value instanceof String && !isAsciiPrintable(((String) value)))
394 attribute.add(((String) value).getBytes(StandardCharsets.UTF_8));
395 else
396 attribute.add(value);
397 Attribute previousAttribute = getModifiedAttributes().put(attribute);
398 if (previousAttribute != null)
399 return previousAttribute.get();
400 else
401 return null;
402 } catch (NamingException e) {
403 throw new IllegalStateException("Cannot get value for attribute " + key, e);
404 }
405 }
406
407 @Override
408 public Object remove(Object key) {
409 getDirectory().checkEdit();
410 if (!isEditing())
411 startEditing();
412
413 if (includeFilter && !attrFilter.contains(key))
414 throw new IllegalArgumentException("Key " + key + " not included");
415 else if (!includeFilter && attrFilter.contains(key))
416 throw new IllegalArgumentException("Key " + key + " excluded");
417
418 try {
419 Attribute attr = getModifiedAttributes().remove(key.toString());
420 if (attr != null)
421 return attr.get();
422 else
423 return null;
424 } catch (NamingException e) {
425 throw new IllegalStateException("Cannot remove attribute " + key, e);
426 }
427 }
428
429 }
430
431 }