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