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