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