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