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