1 package org
.argeo
.osgi
.useradmin
;
3 import static org
.argeo
.osgi
.useradmin
.LdifName
.gidNumber
;
4 import static org
.argeo
.osgi
.useradmin
.LdifName
.homeDirectory
;
5 import static org
.argeo
.osgi
.useradmin
.LdifName
.inetOrgPerson
;
6 import static org
.argeo
.osgi
.useradmin
.LdifName
.objectClass
;
7 import static org
.argeo
.osgi
.useradmin
.LdifName
.organizationalPerson
;
8 import static org
.argeo
.osgi
.useradmin
.LdifName
.person
;
9 import static org
.argeo
.osgi
.useradmin
.LdifName
.posixAccount
;
10 import static org
.argeo
.osgi
.useradmin
.LdifName
.top
;
11 import static org
.argeo
.osgi
.useradmin
.LdifName
.uid
;
12 import static org
.argeo
.osgi
.useradmin
.LdifName
.uidNumber
;
16 import java
.net
.URISyntaxException
;
17 import java
.util
.ArrayList
;
18 import java
.util
.Arrays
;
19 import java
.util
.Dictionary
;
20 import java
.util
.Enumeration
;
21 import java
.util
.Hashtable
;
22 import java
.util
.Iterator
;
23 import java
.util
.List
;
25 import javax
.naming
.InvalidNameException
;
26 import javax
.naming
.NamingException
;
27 import javax
.naming
.directory
.Attributes
;
28 import javax
.naming
.directory
.BasicAttribute
;
29 import javax
.naming
.directory
.BasicAttributes
;
30 import javax
.naming
.ldap
.LdapName
;
31 import javax
.naming
.ldap
.Rdn
;
32 import javax
.transaction
.SystemException
;
33 import javax
.transaction
.Transaction
;
34 import javax
.transaction
.TransactionManager
;
36 import org
.apache
.commons
.logging
.Log
;
37 import org
.apache
.commons
.logging
.LogFactory
;
38 import org
.argeo
.ArgeoException
;
39 import org
.osgi
.framework
.Filter
;
40 import org
.osgi
.framework
.FrameworkUtil
;
41 import org
.osgi
.framework
.InvalidSyntaxException
;
42 import org
.osgi
.service
.useradmin
.Authorization
;
43 import org
.osgi
.service
.useradmin
.Role
;
44 import org
.osgi
.service
.useradmin
.User
;
45 import org
.osgi
.service
.useradmin
.UserAdmin
;
47 /** Base class for a {@link UserDirectory}. */
48 abstract class AbstractUserDirectory
implements UserAdmin
, UserDirectory
{
49 private final static Log log
= LogFactory
50 .getLog(AbstractUserDirectory
.class);
52 private final Hashtable
<String
, Object
> properties
;
53 private final String baseDn
;
54 private final String userObjectClass
;
55 private final String groupObjectClass
;
57 private final boolean readOnly
;
58 private final URI uri
;
60 private UserAdmin externalRoles
;
61 private List
<String
> indexedUserProperties
= Arrays
.asList(new String
[] {
62 LdifName
.uid
.name(), LdifName
.mail
.name(), LdifName
.cn
.name() });
64 private String memberAttributeId
= "member";
65 private List
<String
> credentialAttributeIds
= Arrays
66 .asList(new String
[] { LdifName
.userPassword
.name() });
68 private TransactionManager transactionManager
;
69 // private TransactionSynchronizationRegistry transactionRegistry;
70 // private Xid editingTransactionXid = null;
71 private WcXaResource xaResource
= new WcXaResource(this);
74 private String homeDirectoryBase
= "/home";
76 AbstractUserDirectory(Dictionary
<String
, ?
> props
) {
77 properties
= new Hashtable
<String
, Object
>();
78 for (Enumeration
<String
> keys
= props
.keys(); keys
.hasMoreElements();) {
79 String key
= keys
.nextElement();
80 properties
.put(key
, props
.get(key
));
83 String uriStr
= UserAdminConf
.uri
.getValue(properties
);
88 uri
= new URI(uriStr
);
89 } catch (URISyntaxException e
) {
90 throw new UserDirectoryException("Badly formatted URI "
94 baseDn
= UserAdminConf
.baseDn
.getValue(properties
).toString();
95 String readOnlyStr
= UserAdminConf
.readOnly
.getValue(properties
);
96 if (readOnlyStr
== null) {
97 readOnly
= readOnlyDefault(uri
);
98 properties
.put(UserAdminConf
.readOnly
.property(),
99 Boolean
.toString(readOnly
));
101 readOnly
= new Boolean(readOnlyStr
);
103 userObjectClass
= UserAdminConf
.userObjectClass
.getValue(properties
);
104 groupObjectClass
= UserAdminConf
.groupObjectClass
.getValue(properties
);
107 /** Returns the groups this user is a direct member of. */
108 protected abstract List
<LdapName
> getDirectGroups(LdapName dn
);
110 protected abstract Boolean
daoHasRole(LdapName dn
);
112 protected abstract DirectoryUser
daoGetRole(LdapName key
);
114 protected abstract List
<DirectoryUser
> doGetRoles(Filter f
);
120 public void destroy() {
124 boolean isEditing() {
125 // if (editingTransactionXid == null)
127 // return workingCopy.get() != null;
128 return xaResource
.wc() != null;
131 protected UserDirectoryWorkingCopy
getWorkingCopy() {
132 // UserDirectoryWorkingCopy wc = workingCopy.get();
133 UserDirectoryWorkingCopy wc
= xaResource
.wc();
136 // if (wc.getXid() == null) {
137 // workingCopy.set(null);
144 Transaction transaction
;
146 transaction
= transactionManager
.getTransaction();
147 } catch (SystemException e
) {
148 throw new UserDirectoryException("Cannot get transaction", e
);
150 if (transaction
== null)
151 throw new UserDirectoryException(
152 "A transaction needs to be active in order to edit");
153 if (xaResource
.wc() == null) {
154 // UserDirectoryWorkingCopy wc = new UserDirectoryWorkingCopy(this);
156 transaction
.enlistResource(xaResource
);
157 // editingTransactionXid = wc.getXid();
158 // workingCopy.set(wc);
159 } catch (Exception e
) {
160 throw new UserDirectoryException("Cannot enlist " + xaResource
,
164 // UserDirectoryWorkingCopy wc = xaResource.wc();
166 // throw new UserDirectoryException("Transaction "
167 // + editingTransactionXid + " already editing");
169 // (!editingTransactionXid.equals(workingCopy.get().getXid()))
170 // throw new UserDirectoryException("Working copy Xid "
171 // + workingCopy.get().getXid() + " inconsistent with"
172 // + editingTransactionXid);
176 List
<Role
> getAllRoles(DirectoryUser user
) {
177 List
<Role
> allRoles
= new ArrayList
<Role
>();
179 collectRoles(user
, allRoles
);
182 collectAnonymousRoles(allRoles
);
186 private void collectRoles(DirectoryUser user
, List
<Role
> allRoles
) {
187 for (LdapName groupDn
: getDirectGroups(user
.getDn())) {
188 // TODO check for loops
189 DirectoryUser group
= doGetRole(groupDn
);
191 collectRoles(group
, allRoles
);
195 private void collectAnonymousRoles(List
<Role
> allRoles
) {
196 // TODO gather anonymous roles
201 public Role
getRole(String name
) {
202 return doGetRole(toDn(name
));
205 protected DirectoryUser
doGetRole(LdapName dn
) {
206 UserDirectoryWorkingCopy wc
= getWorkingCopy();
207 DirectoryUser user
= daoGetRole(dn
);
209 if (user
== null && wc
.getNewUsers().containsKey(dn
))
210 user
= wc
.getNewUsers().get(dn
);
211 else if (wc
.getDeletedUsers().containsKey(dn
))
217 @SuppressWarnings("unchecked")
219 public Role
[] getRoles(String filter
) throws InvalidSyntaxException
{
220 UserDirectoryWorkingCopy wc
= getWorkingCopy();
221 Filter f
= filter
!= null ? FrameworkUtil
.createFilter(filter
) : null;
222 List
<DirectoryUser
> res
= doGetRoles(f
);
224 for (Iterator
<DirectoryUser
> it
= res
.iterator(); it
.hasNext();) {
225 DirectoryUser user
= it
.next();
226 LdapName dn
= user
.getDn();
227 if (wc
.getDeletedUsers().containsKey(dn
))
230 for (DirectoryUser user
: wc
.getNewUsers().values()) {
231 if (f
== null || f
.match(user
.getProperties()))
234 // no need to check modified users,
235 // since doGetRoles was already based on the modified attributes
237 return res
.toArray(new Role
[res
.size()]);
241 public User
getUser(String key
, String value
) {
242 // TODO check value null or empty
243 List
<DirectoryUser
> collectedUsers
= new ArrayList
<DirectoryUser
>(
244 getIndexedUserProperties().size());
246 doGetUser(key
, value
, collectedUsers
);
249 DirectoryUser user
= null;
251 user
= (DirectoryUser
) getRole(value
);
253 collectedUsers
.add(user
);
254 } catch (Exception e
) {
258 for (String attr
: getIndexedUserProperties())
259 doGetUser(attr
, value
, collectedUsers
);
261 if (collectedUsers
.size() == 1)
262 return collectedUsers
.get(0);
263 else if (collectedUsers
.size() > 1)
264 log
.warn(collectedUsers
.size() + " users for "
265 + (key
!= null ? key
+ "=" : "") + value
);
269 protected void doGetUser(String key
, String value
,
270 List
<DirectoryUser
> collectedUsers
) {
272 Filter f
= FrameworkUtil
273 .createFilter("(" + key
+ "=" + value
+ ")");
274 List
<DirectoryUser
> users
= doGetRoles(f
);
275 collectedUsers
.addAll(users
);
276 } catch (InvalidSyntaxException e
) {
277 throw new UserDirectoryException("Cannot get user with " + key
283 public Authorization
getAuthorization(User user
) {
284 return new LdifAuthorization((DirectoryUser
) user
,
285 getAllRoles((DirectoryUser
) user
));
289 public Role
createRole(String name
, int type
) {
291 UserDirectoryWorkingCopy wc
= getWorkingCopy();
292 LdapName dn
= toDn(name
);
293 if ((daoHasRole(dn
) && !wc
.getDeletedUsers().containsKey(dn
))
294 || wc
.getNewUsers().containsKey(dn
))
295 throw new UserDirectoryException("Already a role " + name
);
296 BasicAttributes attrs
= new BasicAttributes(true);
297 attrs
.put("dn", dn
.toString());
298 Rdn nameRdn
= dn
.getRdn(dn
.size() - 1);
299 // TODO deal with multiple attr RDN
300 attrs
.put(nameRdn
.getType(), nameRdn
.getValue());
301 if (wc
.getDeletedUsers().containsKey(dn
)) {
302 wc
.getDeletedUsers().remove(dn
);
303 wc
.getModifiedUsers().put(dn
, attrs
);
305 wc
.getModifiedUsers().put(dn
, attrs
);
306 DirectoryUser newRole
= newRole(dn
, type
, attrs
);
307 wc
.getNewUsers().put(dn
, newRole
);
309 return getRole(name
);
312 protected DirectoryUser
newRole(LdapName dn
, int type
, Attributes attrs
) {
314 BasicAttribute objClass
= new BasicAttribute(objectClass
.name());
315 if (type
== Role
.USER
) {
316 String userObjClass
= newUserObjectClass(dn
);
317 objClass
.add(userObjClass
);
318 if (posixAccount
.name().equals(userObjClass
)) {
319 objClass
.add(inetOrgPerson
.name());
320 objClass
.add(organizationalPerson
.name());
321 objClass
.add(person
.name());
325 username
= dn
.getRdn(dn
.size() - 1).toAttributes()
326 .get(uid
.name()).get().toString();
327 } catch (NamingException e
) {
328 throw new UserDirectoryException(
329 "Cannot extract username from " + dn
, e
);
331 // TODO look for uid in attributes too?
332 attrs
.put(uidNumber
.name(), new Long(max(uidNumber
.name()) + 1));
333 attrs
.put(homeDirectory
.name(), generateHomeDirectory(username
));
334 // TODO create user private group
335 // NB: on RHEL, the 'users' group has gid 100
336 attrs
.put(gidNumber
.name(), 100);
337 // attrs.put(LdifName.loginShell.name(),"/sbin/nologin");
338 } else if (inetOrgPerson
.name().equals(userObjClass
)) {
339 objClass
.add(organizationalPerson
.name());
340 objClass
.add(person
.name());
341 } else if (organizationalPerson
.name().equals(userObjClass
)) {
342 objClass
.add(person
.name());
346 newRole
= new LdifUser(this, dn
, attrs
);
347 } else if (type
== Role
.GROUP
) {
348 String groupObjClass
= getGroupObjectClass();
349 objClass
.add(groupObjClass
);
350 objClass
.add(LdifName
.extensibleObject
.name());
351 attrs
.put(gidNumber
.name(), new Long(max(gidNumber
.name()) + 1));
354 newRole
= new LdifGroup(this, dn
, attrs
);
356 throw new UserDirectoryException("Unsupported type " + type
);
361 public boolean removeRole(String name
) {
363 UserDirectoryWorkingCopy wc
= getWorkingCopy();
364 LdapName dn
= toDn(name
);
365 boolean actuallyDeleted
;
366 if (daoHasRole(dn
) || wc
.getNewUsers().containsKey(dn
)) {
367 DirectoryUser user
= (DirectoryUser
) getRole(name
);
368 wc
.getDeletedUsers().put(dn
, user
);
369 actuallyDeleted
= true;
370 } else {// just removing from groups (e.g. system roles)
371 actuallyDeleted
= false;
373 for (LdapName groupDn
: getDirectGroups(dn
)) {
374 DirectoryUser group
= doGetRole(groupDn
);
375 group
.getAttributes().get(getMemberAttributeId())
376 .remove(dn
.toString());
378 return actuallyDeleted
;
382 /** Generate path for a new user home */
383 protected String
generateHomeDirectory(String username
) {
384 String base
= homeDirectoryBase
;
385 int atIndex
= username
.indexOf('@');
387 String domain
= username
.substring(0, atIndex
);
388 String name
= username
.substring(atIndex
+ 1);
389 return base
+ '/' + firstCharsToPath(domain
, 2) + '/' + domain
390 + '/' + firstCharsToPath(name
, 2) + '/' + name
;
391 } else if (atIndex
== 0 || atIndex
== (username
.length() - 1)) {
392 throw new ArgeoException("Unsupported username " + username
);
394 return base
+ '/' + firstCharsToPath(username
, 2) + '/' + username
;
398 protected long max(String attr
) {
401 List
<DirectoryUser
> users
= doGetRoles(FrameworkUtil
402 .createFilter("(" + attr
+ "=*)"));
404 for (DirectoryUser user
: users
) {
405 long uid
= Long
.parseLong(user
.getAttributes().get(attr
).get()
410 } catch (Exception e
) {
411 throw new UserDirectoryException("Cannot get max of " + attr
, e
);
417 * Creates depth from a string (typically a username) by adding levels based
418 * on its first characters: "aBcD",2 => a/aB
420 public static String
firstCharsToPath(String str
, Integer nbrOfChars
) {
421 if (str
.length() < nbrOfChars
)
422 throw new ArgeoException("String " + str
423 + " length must be greater or equal than " + nbrOfChars
);
424 StringBuffer path
= new StringBuffer("");
425 StringBuffer curr
= new StringBuffer("");
426 for (int i
= 0; i
< nbrOfChars
; i
++) {
427 curr
.append(str
.charAt(i
));
429 if (i
< nbrOfChars
- 1)
432 return path
.toString();
436 protected void prepare(UserDirectoryWorkingCopy wc
) {
440 protected void commit(UserDirectoryWorkingCopy wc
) {
444 protected void rollback(UserDirectoryWorkingCopy wc
) {
448 // void clearEditingTransactionXid() {
449 // editingTransactionXid = null;
453 protected LdapName
toDn(String name
) {
455 return new LdapName(name
);
456 } catch (InvalidNameException e
) {
457 throw new UserDirectoryException("Badly formatted name", e
);
463 String
getMemberAttributeId() {
464 return memberAttributeId
;
467 List
<String
> getCredentialAttributeIds() {
468 return credentialAttributeIds
;
471 protected URI
getUri() {
475 protected List
<String
> getIndexedUserProperties() {
476 return indexedUserProperties
;
479 protected void setIndexedUserProperties(List
<String
> indexedUserProperties
) {
480 this.indexedUserProperties
= indexedUserProperties
;
483 private static boolean readOnlyDefault(URI uri
) {
486 if (uri
.getScheme().equals("file")) {
487 File file
= new File(uri
);
489 return !file
.canWrite();
491 return !file
.getParentFile().canWrite();
496 public boolean isReadOnly() {
500 UserAdmin
getExternalRoles() {
501 return externalRoles
;
504 public String
getBaseDn() {
508 /** dn can be null, in that case a default should be returned. */
509 protected String
getUserObjectClass() {
510 return userObjectClass
;
513 protected String
newUserObjectClass(LdapName dn
) {
515 && dn
.getRdn(dn
.size() - 1).toAttributes().get(uid
.name()) != null)
516 return posixAccount
.name();
518 return getUserObjectClass();
521 protected String
getGroupObjectClass() {
522 return groupObjectClass
;
525 public Dictionary
<String
, ?
> getProperties() {
529 public void setExternalRoles(UserAdmin externalRoles
) {
530 this.externalRoles
= externalRoles
;
533 public void setTransactionManager(TransactionManager transactionManager
) {
534 this.transactionManager
= transactionManager
;
537 public WcXaResource
getXaResource() {