1 package org
.argeo
.osgi
.useradmin
;
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
;
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
;
22 import org
.argeo
.naming
.AuthPassword
;
23 import org
.argeo
.naming
.LdapAttrs
;
24 import org
.argeo
.naming
.SharedSecret
;
26 /** Directory user implementation */
27 class LdifUser
implements DirectoryUser
{
28 private final AbstractUserDirectory userAdmin
;
30 private final LdapName dn
;
32 private final boolean frozen
;
33 private Attributes publishedAttributes
;
35 private final AttributeDictionary properties
;
36 private final AttributeDictionary credentials
;
38 LdifUser(AbstractUserDirectory userAdmin
, LdapName dn
, Attributes attributes
) {
39 this(userAdmin
, dn
, attributes
, false);
42 private LdifUser(AbstractUserDirectory userAdmin
, LdapName dn
, Attributes attributes
, boolean frozen
) {
43 this.userAdmin
= userAdmin
;
45 this.publishedAttributes
= attributes
;
46 properties
= new AttributeDictionary(false);
47 credentials
= new AttributeDictionary(true);
52 public String
getName() {
57 public int getType() {
62 public Dictionary
<String
, Object
> getProperties() {
67 public Dictionary
<String
, Object
> getCredentials() {
72 public boolean hasCredential(String key
, Object value
) {
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);
86 // boolean wasRemoved = AuthPassword.remove(getAttributes(), onceToken);
89 // TODO delete expired tokens?
92 throw new UnsupportedOperationException(
93 "Unsupported authPassword scheme " + authPassword
.getAuthScheme());
98 byte[] hashedPassword
= hash(password
);
99 if (hasCredential(LdapAttrs
.userPassword
.name(), hashedPassword
))
101 // if (hasCredential(LdapAttrs.authPassword.name(), pwd))
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);
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);
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()))
129 // throw new UnsupportedOperationException("An expiry timestamp "
130 // + LdapAttrs.modifyTimestamp.name() + " must be set in the URI query");
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);
141 Object storedValue
= getCredentials().get(key
);
142 if (storedValue
== null || value
== null)
144 if (!(value
instanceof String
|| value
instanceof byte[]))
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
);
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
;
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
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
188 public LdapName
getDn() {
193 public synchronized Attributes
getAttributes() {
194 return isEditing() ?
getModifiedAttributes() : publishedAttributes
;
197 /** Should only be called from working copy thread. */
198 private synchronized Attributes
getModifiedAttributes() {
199 assert getWc() != null;
200 return getWc().getAttributes(getDn());
203 protected synchronized boolean isEditing() {
204 return getWc() != null && getModifiedAttributes() != null;
207 private synchronized UserDirectoryWorkingCopy
getWc() {
208 return userAdmin
.getWorkingCopy();
211 protected synchronized void startEditing() {
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();
221 public synchronized void publishAttributes(Attributes modifiedAttributes
) {
222 publishedAttributes
= modifiedAttributes
;
225 public DirectoryUser
getPublished() {
226 return new LdifUser(userAdmin
, dn
, publishedAttributes
, true);
230 public int hashCode() {
231 return dn
.hashCode();
235 public boolean equals(Object obj
) {
238 if (obj
instanceof LdifUser
) {
239 LdifUser that
= (LdifUser
) obj
;
240 return this.dn
.equals(that
.dn
);
246 public String
toString() {
247 return dn
.toString();
250 protected AbstractUserDirectory
getUserAdmin() {
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
;
259 public AttributeDictionary(Boolean includeFilter
) {
260 this.attrFilter
= userAdmin
.getCredentialAttributeIds();
261 this.includeFilter
= includeFilter
;
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
);
271 } catch (NamingException e
) {
272 throw new UserDirectoryException("Cannot initialise attribute dictionary", e
);
278 return effectiveKeys
.size();
282 public boolean isEmpty() {
283 return effectiveKeys
.size() == 0;
287 public Enumeration
<String
> keys() {
288 return Collections
.enumeration(effectiveKeys
);
292 public Enumeration
<Object
> elements() {
293 final Iterator
<String
> it
= effectiveKeys
.iterator();
294 return new Enumeration
<Object
>() {
297 public boolean hasMoreElements() {
302 public Object
nextElement() {
303 String key
= it
.next();
311 public Object
get(Object key
) {
313 Attribute attr
= getAttributes().get(key
.toString());
316 Object value
= attr
.get();
317 if (value
instanceof byte[]) {
318 if (key
.equals(LdapAttrs
.userPassword
.name()))
319 // TODO other cases (certificates, images)
321 value
= new String((byte[]) value
, StandardCharsets
.UTF_8
);
323 if (attr
.size() == 1)
325 if (!attr
.getID().equals(LdapAttrs
.objectClass
.name()))
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
);
335 if (objectClasses
.contains(userAdmin
.getUserObjectClass()))
336 return userAdmin
.getUserObjectClass();
337 else if (objectClasses
.contains(userAdmin
.getGroupObjectClass()))
338 return userAdmin
.getGroupObjectClass();
341 } catch (NamingException e
) {
342 throw new UserDirectoryException("Cannot get value for attribute " + key
, e
);
347 public Object
put(String key
, Object value
) {
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
);
354 if (key
.startsWith("X-")) {
355 return put(LdapAttrs
.authPassword
.name(), value
);
358 userAdmin
.checkEdit();
362 if (!(value
instanceof String
|| value
instanceof byte[]))
363 throw new IllegalArgumentException("Value must be String or byte[]");
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");
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
));
377 attribute
.add(value
);
378 Attribute previousAttribute
= getModifiedAttributes().put(attribute
);
379 if (previousAttribute
!= null)
380 return previousAttribute
.get();
383 } catch (NamingException e
) {
384 throw new UserDirectoryException("Cannot get value for attribute " + key
, e
);
389 public Object
remove(Object key
) {
390 userAdmin
.checkEdit();
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");
400 Attribute attr
= getModifiedAttributes().remove(key
.toString());
405 } catch (NamingException e
) {
406 throw new UserDirectoryException("Cannot remove attribute " + key
, e
);
411 private static boolean isAsciiPrintable(String str
) {
415 int sz
= str
.length();
416 for (int i
= 0; i
< sz
; i
++) {
417 if (isAsciiPrintable(str
.charAt(i
)) == false) {
424 private static boolean isAsciiPrintable(char ch
) {
425 return ch
>= 32 && ch
< 127;