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