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