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