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