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