1 package org
.argeo
.osgi
.useradmin
;
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
;
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
;
24 import org
.argeo
.naming
.AuthPassword
;
25 import org
.argeo
.naming
.LdapAttrs
;
26 import org
.argeo
.naming
.SharedSecret
;
28 /** Directory user implementation */
29 class LdifUser
implements DirectoryUser
{
30 private final AbstractUserDirectory userAdmin
;
32 private final LdapName dn
;
34 private final boolean frozen
;
35 private Attributes publishedAttributes
;
37 private final AttributeDictionary properties
;
38 private final AttributeDictionary credentials
;
40 LdifUser(AbstractUserDirectory userAdmin
, LdapName dn
, Attributes attributes
) {
41 this(userAdmin
, dn
, attributes
, false);
44 private LdifUser(AbstractUserDirectory userAdmin
, LdapName dn
, Attributes attributes
, boolean frozen
) {
45 this.userAdmin
= userAdmin
;
47 this.publishedAttributes
= attributes
;
48 properties
= new AttributeDictionary(false);
49 credentials
= new AttributeDictionary(true);
54 public String
getName() {
59 public int getType() {
64 public Dictionary
<String
, Object
> getProperties() {
69 public Dictionary
<String
, Object
> getCredentials() {
74 public boolean hasCredential(String key
, Object value
) {
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);
88 // boolean wasRemoved = AuthPassword.remove(getAttributes(), onceToken);
91 // TODO delete expired tokens?
94 throw new UnsupportedOperationException(
95 "Unsupported authPassword scheme " + authPassword
.getAuthScheme());
100 byte[] hashedPassword
= hash(password
);
101 if (hasCredential(LdapAttrs
.userPassword
.name(), hashedPassword
))
103 // if (hasCredential(LdapAttrs.authPassword.name(), pwd))
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);
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);
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()))
131 // throw new UnsupportedOperationException("An expiry timestamp "
132 // + LdapAttrs.modifyTimestamp.name() + " must be set in the URI query");
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);
143 Object storedValue
= getCredentials().get(key
);
144 if (storedValue
== null || value
== null)
146 if (!(value
instanceof String
|| value
instanceof byte[]))
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
);
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
;
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
172 private char[] toChars(Object obj
) {
173 if (obj
instanceof char[])
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
187 public LdapName
getDn() {
192 public synchronized Attributes
getAttributes() {
193 return isEditing() ?
getModifiedAttributes() : publishedAttributes
;
196 /** Should only be called from working copy thread. */
197 private synchronized Attributes
getModifiedAttributes() {
198 assert getWc() != null;
199 return getWc().getAttributes(getDn());
202 protected synchronized boolean isEditing() {
203 return getWc() != null && getModifiedAttributes() != null;
206 private synchronized UserDirectoryWorkingCopy
getWc() {
207 return userAdmin
.getWorkingCopy();
210 protected synchronized void startEditing() {
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();
220 public synchronized void publishAttributes(Attributes modifiedAttributes
) {
221 publishedAttributes
= modifiedAttributes
;
224 public DirectoryUser
getPublished() {
225 return new LdifUser(userAdmin
, dn
, publishedAttributes
, true);
229 public int hashCode() {
230 return dn
.hashCode();
234 public boolean equals(Object obj
) {
237 if (obj
instanceof LdifUser
) {
238 LdifUser that
= (LdifUser
) obj
;
239 return this.dn
.equals(that
.dn
);
245 public String
toString() {
246 return dn
.toString();
249 protected AbstractUserDirectory
getUserAdmin() {
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
;
258 public AttributeDictionary(Boolean includeFilter
) {
259 this.attrFilter
= userAdmin
.getCredentialAttributeIds();
260 this.includeFilter
= includeFilter
;
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
);
270 } catch (NamingException e
) {
271 throw new UserDirectoryException("Cannot initialise attribute dictionary", e
);
277 return effectiveKeys
.size();
281 public boolean isEmpty() {
282 return effectiveKeys
.size() == 0;
286 public Enumeration
<String
> keys() {
287 return Collections
.enumeration(effectiveKeys
);
291 public Enumeration
<Object
> elements() {
292 final Iterator
<String
> it
= effectiveKeys
.iterator();
293 return new Enumeration
<Object
>() {
296 public boolean hasMoreElements() {
301 public Object
nextElement() {
302 String key
= it
.next();
310 public Object
get(Object key
) {
312 Attribute attr
= getAttributes().get(key
.toString());
315 Object value
= attr
.get();
316 if (value
instanceof byte[]) {
317 if (key
.equals(LdapAttrs
.userPassword
.name()))
318 // TODO other cases (certificates, images)
320 value
= new String((byte[]) value
, StandardCharsets
.UTF_8
);
322 if (attr
.size() == 1)
324 if (!attr
.getID().equals(LdapAttrs
.objectClass
.name()))
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
);
334 if (objectClasses
.contains(userAdmin
.getUserObjectClass()))
335 return userAdmin
.getUserObjectClass();
336 else if (objectClasses
.contains(userAdmin
.getGroupObjectClass()))
337 return userAdmin
.getGroupObjectClass();
340 } catch (NamingException e
) {
341 throw new UserDirectoryException("Cannot get value for attribute " + key
, e
);
346 public Object
put(String key
, Object value
) {
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
);
353 if (key
.startsWith("X-")) {
354 return put(LdapAttrs
.authPassword
.name(), value
);
357 userAdmin
.checkEdit();
361 if (!(value
instanceof String
|| value
instanceof byte[]))
362 throw new IllegalArgumentException("Value must be String or byte[]");
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");
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
));
376 attribute
.add(value
);
377 Attribute previousAttribute
= getModifiedAttributes().put(attribute
);
378 if (previousAttribute
!= null)
379 return previousAttribute
.get();
382 } catch (NamingException e
) {
383 throw new UserDirectoryException("Cannot get value for attribute " + key
, e
);
388 public Object
remove(Object key
) {
389 userAdmin
.checkEdit();
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");
399 Attribute attr
= getModifiedAttributes().remove(key
.toString());
404 } catch (NamingException e
) {
405 throw new UserDirectoryException("Cannot remove attribute " + key
, e
);
410 private static boolean isAsciiPrintable(String str
) {
414 int sz
= str
.length();
415 for (int i
= 0; i
< sz
; i
++) {
416 if (isAsciiPrintable(str
.charAt(i
)) == false) {
423 private static boolean isAsciiPrintable(char ch
) {
424 return ch
>= 32 && ch
< 127;