]> 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.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.SharedSecret;
29
30 /** Directory user implementation */
31 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().getAttributes(getDn());
187 }
188
189 protected synchronized boolean isEditing() {
190 return getWc() != null && getModifiedAttributes() != null;
191 }
192
193 private synchronized UserDirectoryWorkingCopy getWc() {
194 return userAdmin.getWorkingCopy();
195 }
196
197 protected synchronized void startEditing() {
198 if (frozen)
199 throw new UserDirectoryException("Cannot edit frozen view");
200 if (getUserAdmin().isReadOnly())
201 throw new UserDirectoryException("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 includeFilter) {
246 this.attrFilter = userAdmin.getCredentialAttributeIds();
247 this.includeFilter = includeFilter;
248 try {
249 NamingEnumeration<String> ids = getAttributes().getIDs();
250 while (ids.hasMore()) {
251 String id = ids.next();
252 if (includeFilter && attrFilter.contains(id))
253 effectiveKeys.add(id);
254 else if (!includeFilter && !attrFilter.contains(id))
255 effectiveKeys.add(id);
256 }
257 } catch (NamingException e) {
258 throw new UserDirectoryException("Cannot initialise attribute dictionary", e);
259 }
260 }
261
262 @Override
263 public int size() {
264 return effectiveKeys.size();
265 }
266
267 @Override
268 public boolean isEmpty() {
269 return effectiveKeys.size() == 0;
270 }
271
272 @Override
273 public Enumeration<String> keys() {
274 return Collections.enumeration(effectiveKeys);
275 }
276
277 @Override
278 public Enumeration<Object> elements() {
279 final Iterator<String> it = effectiveKeys.iterator();
280 return new Enumeration<Object>() {
281
282 @Override
283 public boolean hasMoreElements() {
284 return it.hasNext();
285 }
286
287 @Override
288 public Object nextElement() {
289 String key = it.next();
290 return get(key);
291 }
292
293 };
294 }
295
296 @Override
297 public Object get(Object key) {
298 try {
299 Attribute attr = getAttributes().get(key.toString());
300 if (attr == null)
301 return null;
302 Object value = attr.get();
303 if (value instanceof byte[]) {
304 if (key.equals(LdapAttrs.userPassword.name()))
305 // TODO other cases (certificates, images)
306 return value;
307 value = new String((byte[]) value, StandardCharsets.UTF_8);
308 }
309 if (attr.size() == 1)
310 return value;
311 // if (!attr.getID().equals(LdapAttrs.objectClass.name()))
312 // return value;
313 // special case for object class
314 NamingEnumeration<?> en = attr.getAll();
315 StringJoiner values = new StringJoiner("\n");
316 // Set<String> values = new HashSet<String>();
317 while (en.hasMore()) {
318 String v = en.next().toString();
319 values.add(v);
320 }
321 return values.toString();
322 // if (objectClasses.contains(userAdmin.getUserObjectClass()))
323 // return userAdmin.getUserObjectClass();
324 // else if (objectClasses.contains(userAdmin.getGroupObjectClass()))
325 // return userAdmin.getGroupObjectClass();
326 // else
327 // return value;
328 } catch (NamingException e) {
329 throw new IllegalStateException("Cannot get value for attribute " + key, e);
330 }
331 }
332
333 @Override
334 public Object put(String key, Object value) {
335 if (key == null) {
336 // TODO persist to other sources (like PKCS12)
337 char[] password = DigestUtils.bytesToChars(value);
338 byte[] hashedPassword = sha1hash(password);
339 return put(LdapAttrs.userPassword.name(), hashedPassword);
340 }
341 if (key.startsWith("X-")) {
342 return put(LdapAttrs.authPassword.name(), value);
343 }
344
345 userAdmin.checkEdit();
346 if (!isEditing())
347 startEditing();
348
349 if (!(value instanceof String || value instanceof byte[]))
350 throw new IllegalArgumentException("Value must be String or byte[]");
351
352 if (includeFilter && !attrFilter.contains(key))
353 throw new IllegalArgumentException("Key " + key + " not included");
354 else if (!includeFilter && attrFilter.contains(key))
355 throw new IllegalArgumentException("Key " + key + " excluded");
356
357 try {
358 Attribute attribute = getModifiedAttributes().get(key.toString());
359 // if (attribute == null) // block unit tests
360 attribute = new BasicAttribute(key.toString());
361 if (value instanceof String && !isAsciiPrintable(((String) value)))
362 attribute.add(((String) value).getBytes(StandardCharsets.UTF_8));
363 else
364 attribute.add(value);
365 Attribute previousAttribute = getModifiedAttributes().put(attribute);
366 if (previousAttribute != null)
367 return previousAttribute.get();
368 else
369 return null;
370 } catch (NamingException e) {
371 throw new UserDirectoryException("Cannot get value for attribute " + key, e);
372 }
373 }
374
375 @Override
376 public Object remove(Object key) {
377 userAdmin.checkEdit();
378 if (!isEditing())
379 startEditing();
380
381 if (includeFilter && !attrFilter.contains(key))
382 throw new IllegalArgumentException("Key " + key + " not included");
383 else if (!includeFilter && attrFilter.contains(key))
384 throw new IllegalArgumentException("Key " + key + " excluded");
385
386 try {
387 Attribute attr = getModifiedAttributes().remove(key.toString());
388 if (attr != null)
389 return attr.get();
390 else
391 return null;
392 } catch (NamingException e) {
393 throw new UserDirectoryException("Cannot remove attribute " + key, e);
394 }
395 }
396 }
397
398 private static boolean isAsciiPrintable(String str) {
399 if (str == null) {
400 return false;
401 }
402 int sz = str.length();
403 for (int i = 0; i < sz; i++) {
404 if (isAsciiPrintable(str.charAt(i)) == false) {
405 return false;
406 }
407 }
408 return true;
409 }
410
411 private static boolean isAsciiPrintable(char ch) {
412 return ch >= 32 && ch < 127;
413 }
414
415 }