1 package org
.argeo
.osgi
.useradmin
;
3 import static java
.nio
.charset
.StandardCharsets
.US_ASCII
;
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
.Iterator
;
14 import java
.util
.List
;
15 import java
.util
.StringJoiner
;
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
.util
.directory
.Person
;
25 import org
.argeo
.util
.naming
.LdapAttrs
;
26 import org
.argeo
.util
.naming
.LdapObjs
;
27 import org
.argeo
.util
.naming
.SharedSecret
;
28 import org
.argeo
.util
.naming
.ldap
.AuthPassword
;
30 /** Directory user implementation */
31 abstract class LdifUser
implements DirectoryUser
{
32 private final AbstractUserDirectory userAdmin
;
34 private final LdapName dn
;
36 private final boolean frozen
;
37 private Attributes publishedAttributes
;
39 private final AttributeDictionary properties
;
40 private final AttributeDictionary credentials
;
42 LdifUser(AbstractUserDirectory userAdmin
, LdapName dn
, Attributes attributes
) {
43 this(userAdmin
, dn
, attributes
, false);
46 private LdifUser(AbstractUserDirectory userAdmin
, LdapName dn
, Attributes attributes
, boolean frozen
) {
47 this.userAdmin
= userAdmin
;
49 this.publishedAttributes
= attributes
;
50 properties
= new AttributeDictionary(false);
51 credentials
= new AttributeDictionary(true);
56 public String
getName() {
61 public int getType() {
66 public Dictionary
<String
, Object
> getProperties() {
71 public Dictionary
<String
, Object
> getCredentials() {
76 public boolean hasCredential(String key
, Object value
) {
78 // TODO check other sources (like PKCS12)
79 // String pwd = new String((char[]) value);
80 // authPassword (RFC 312 https://tools.ietf.org/html/rfc3112)
81 char[] password
= DigestUtils
.bytesToChars(value
);
83 if (userAdmin
.getForcedPassword() != null && userAdmin
.getForcedPassword().equals(new String(password
)))
86 AuthPassword authPassword
= AuthPassword
.matchAuthValue(getAttributes(), password
);
87 if (authPassword
!= null) {
88 if (authPassword
.getAuthScheme().equals(SharedSecret
.X_SHARED_SECRET
)) {
89 SharedSecret onceToken
= new SharedSecret(authPassword
);
90 if (onceToken
.isExpired()) {
91 // AuthPassword.remove(getAttributes(), onceToken);
94 // boolean wasRemoved = AuthPassword.remove(getAttributes(), onceToken);
97 // TODO delete expired tokens?
100 throw new UnsupportedOperationException(
101 "Unsupported authPassword scheme " + authPassword
.getAuthScheme());
106 // byte[] hashedPassword = hash(password, DigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256);
107 if (hasCredential(LdapAttrs
.userPassword
.name(), DigestUtils
.charsToBytes(password
)))
112 Object storedValue
= getCredentials().get(key
);
113 if (storedValue
== null || value
== null)
115 if (!(value
instanceof String
|| value
instanceof byte[]))
117 if (storedValue
instanceof String
&& value
instanceof String
)
118 return storedValue
.equals(value
);
119 if (storedValue
instanceof byte[] && value
instanceof byte[]) {
120 String storedBase64
= new String((byte[]) storedValue
, US_ASCII
);
121 String passwordScheme
= null;
122 if (storedBase64
.charAt(0) == '{') {
123 int index
= storedBase64
.indexOf('}');
125 passwordScheme
= storedBase64
.substring(1, index
);
126 String storedValueBase64
= storedBase64
.substring(index
+ 1);
127 byte[] storedValueBytes
= Base64
.getDecoder().decode(storedValueBase64
);
128 char[] passwordValue
= DigestUtils
.bytesToChars((byte[]) value
);
130 if (DigestUtils
.PASSWORD_SCHEME_SHA
.equals(passwordScheme
)) {
131 valueBytes
= DigestUtils
.toPasswordScheme(passwordScheme
, passwordValue
, null, null, null);
132 } else if (DigestUtils
.PASSWORD_SCHEME_PBKDF2_SHA256
.equals(passwordScheme
)) {
133 // see https://www.thesubtlety.com/post/a-389-ds-pbkdf2-password-checker/
134 byte[] iterationsArr
= Arrays
.copyOfRange(storedValueBytes
, 0, 4);
135 BigInteger iterations
= new BigInteger(iterationsArr
);
136 byte[] salt
= Arrays
.copyOfRange(storedValueBytes
, iterationsArr
.length
,
137 iterationsArr
.length
+ 64);
138 byte[] keyArr
= Arrays
.copyOfRange(storedValueBytes
, iterationsArr
.length
+ salt
.length
,
139 storedValueBytes
.length
);
140 int keyLengthBits
= keyArr
.length
* 8;
141 valueBytes
= DigestUtils
.toPasswordScheme(passwordScheme
, passwordValue
, salt
,
142 iterations
.intValue(), keyLengthBits
);
144 throw new UnsupportedOperationException("Unknown password scheme " + passwordScheme
);
146 return Arrays
.equals(storedValueBytes
, valueBytes
);
150 // if (storedValue instanceof byte[] && value instanceof byte[]) {
151 // return Arrays.equals((byte[]) storedValue, (byte[]) value);
156 /** Hash the password */
157 byte[] sha1hash(char[] password
) {
158 byte[] hashedPassword
= ("{SHA}"
159 + Base64
.getEncoder().encodeToString(DigestUtils
.sha1(DigestUtils
.charsToBytes(password
))))
160 .getBytes(StandardCharsets
.UTF_8
);
161 return hashedPassword
;
164 // byte[] hash(char[] password, String passwordScheme) {
165 // if (passwordScheme == null)
166 // passwordScheme = DigestUtils.PASSWORD_SCHEME_SHA;
167 // byte[] hashedPassword = ("{" + passwordScheme + "}"
168 // + Base64.getEncoder().encodeToString(DigestUtils.toPasswordScheme(passwordScheme, password)))
169 // .getBytes(US_ASCII);
170 // return hashedPassword;
174 public LdapName
getDn() {
179 public synchronized Attributes
getAttributes() {
180 return isEditing() ?
getModifiedAttributes() : publishedAttributes
;
183 /** Should only be called from working copy thread. */
184 private synchronized Attributes
getModifiedAttributes() {
185 assert getWc() != null;
186 return getWc().getModifiedData().get(getDn());
189 protected synchronized boolean isEditing() {
190 return getWc() != null && getModifiedAttributes() != null;
193 private synchronized DirectoryUserWorkingCopy
getWc() {
194 return userAdmin
.getWorkingCopy();
197 protected synchronized void startEditing() {
199 throw new IllegalStateException("Cannot edit frozen view");
200 if (getUserAdmin().isReadOnly())
201 throw new IllegalStateException("User directory is read-only");
202 assert getModifiedAttributes() == null;
203 getWc().startEditing(this);
204 // modifiedAttributes = (Attributes) publishedAttributes.clone();
207 public synchronized void publishAttributes(Attributes modifiedAttributes
) {
208 publishedAttributes
= modifiedAttributes
;
211 // public DirectoryUser getPublished() {
212 // return new LdifUser(userAdmin, dn, publishedAttributes, true);
216 public int hashCode() {
217 return dn
.hashCode();
221 public boolean equals(Object obj
) {
224 if (obj
instanceof LdifUser
) {
225 LdifUser that
= (LdifUser
) obj
;
226 return this.dn
.equals(that
.dn
);
232 public String
toString() {
233 return dn
.toString();
236 protected AbstractUserDirectory
getUserAdmin() {
240 private class AttributeDictionary
extends Dictionary
<String
, Object
> {
241 private final List
<String
> effectiveKeys
= new ArrayList
<String
>();
242 private final List
<String
> attrFilter
;
243 private final Boolean includeFilter
;
245 public AttributeDictionary(Boolean credentials
) {
246 this.attrFilter
= userAdmin
.getCredentialAttributeIds();
247 this.includeFilter
= credentials
;
249 NamingEnumeration
<String
> ids
= getAttributes().getIDs();
250 while (ids
.hasMore()) {
251 String id
= ids
.next();
252 if (credentials
&& attrFilter
.contains(id
))
253 effectiveKeys
.add(id
);
254 else if (!credentials
&& !attrFilter
.contains(id
))
255 effectiveKeys
.add(id
);
257 } catch (NamingException e
) {
258 throw new IllegalStateException("Cannot initialise attribute dictionary", e
);
261 effectiveKeys
.add(LdapAttrs
.objectClasses
.name());
266 return effectiveKeys
.size();
270 public boolean isEmpty() {
271 return effectiveKeys
.size() == 0;
275 public Enumeration
<String
> keys() {
276 return Collections
.enumeration(effectiveKeys
);
280 public Enumeration
<Object
> elements() {
281 final Iterator
<String
> it
= effectiveKeys
.iterator();
282 return new Enumeration
<Object
>() {
285 public boolean hasMoreElements() {
290 public Object
nextElement() {
291 String key
= it
.next();
299 public Object
get(Object key
) {
301 Attribute attr
= !key
.equals(LdapAttrs
.objectClasses
.name()) ?
getAttributes().get(key
.toString())
302 : getAttributes().get(LdapAttrs
.objectClass
.name());
305 Object value
= attr
.get();
306 if (value
instanceof byte[]) {
307 if (key
.equals(LdapAttrs
.userPassword
.name()))
308 // TODO other cases (certificates, images)
310 value
= new String((byte[]) value
, StandardCharsets
.UTF_8
);
312 if (attr
.size() == 1)
314 // special case for object class
315 if (key
.equals(LdapAttrs
.objectClass
.name())) {
316 // TODO support multiple object classes
317 NamingEnumeration
<?
> en
= attr
.getAll();
319 attrs
: while (en
.hasMore()) {
320 String v
= en
.next().toString();
321 if (v
.equalsIgnoreCase(LdapObjs
.top
.name()))
325 if (v
.equalsIgnoreCase(userAdmin
.getUserObjectClass()))
326 return userAdmin
.getUserObjectClass();
327 else if (v
.equalsIgnoreCase(userAdmin
.getGroupObjectClass()))
328 return userAdmin
.getGroupObjectClass();
332 throw new IllegalStateException("Cannot find objectClass in " + value
);
334 NamingEnumeration
<?
> en
= attr
.getAll();
335 StringJoiner values
= new StringJoiner("\n");
336 while (en
.hasMore()) {
337 String v
= en
.next().toString();
340 return values
.toString();
344 } catch (NamingException e
) {
345 throw new IllegalStateException("Cannot get value for attribute " + key
, e
);
350 public Object
put(String key
, Object value
) {
352 // TODO persist to other sources (like PKCS12)
353 char[] password
= DigestUtils
.bytesToChars(value
);
354 byte[] hashedPassword
= sha1hash(password
);
355 return put(LdapAttrs
.userPassword
.name(), hashedPassword
);
357 if (key
.startsWith("X-")) {
358 return put(LdapAttrs
.authPassword
.name(), value
);
361 userAdmin
.checkEdit();
365 if (!(value
instanceof String
|| value
instanceof byte[]))
366 throw new IllegalArgumentException("Value must be String or byte[]");
368 if (includeFilter
&& !attrFilter
.contains(key
))
369 throw new IllegalArgumentException("Key " + key
+ " not included");
370 else if (!includeFilter
&& attrFilter
.contains(key
))
371 throw new IllegalArgumentException("Key " + key
+ " excluded");
374 Attribute attribute
= getModifiedAttributes().get(key
.toString());
375 // if (attribute == null) // block unit tests
376 attribute
= new BasicAttribute(key
.toString());
377 if (value
instanceof String
&& !isAsciiPrintable(((String
) value
)))
378 attribute
.add(((String
) value
).getBytes(StandardCharsets
.UTF_8
));
380 attribute
.add(value
);
381 Attribute previousAttribute
= getModifiedAttributes().put(attribute
);
382 if (previousAttribute
!= null)
383 return previousAttribute
.get();
386 } catch (NamingException e
) {
387 throw new IllegalStateException("Cannot get value for attribute " + key
, e
);
392 public Object
remove(Object key
) {
393 userAdmin
.checkEdit();
397 if (includeFilter
&& !attrFilter
.contains(key
))
398 throw new IllegalArgumentException("Key " + key
+ " not included");
399 else if (!includeFilter
&& attrFilter
.contains(key
))
400 throw new IllegalArgumentException("Key " + key
+ " excluded");
403 Attribute attr
= getModifiedAttributes().remove(key
.toString());
408 } catch (NamingException e
) {
409 throw new IllegalStateException("Cannot remove attribute " + key
, e
);
414 private static boolean isAsciiPrintable(String str
) {
418 int sz
= str
.length();
419 for (int i
= 0; i
< sz
; i
++) {
420 if (isAsciiPrintable(str
.charAt(i
)) == false) {
427 private static boolean isAsciiPrintable(char ch
) {
428 return ch
>= 32 && ch
< 127;
431 static class LdifPerson
extends LdifUser
implements Person
{
433 public LdifPerson(AbstractUserDirectory userAdmin
, LdapName dn
, Attributes attributes
) {
434 super(userAdmin
, dn
, attributes
);