]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.security.core/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java
Fix XA error codes
[lgpl/argeo-commons.git] / org.argeo.security.core / src / org / argeo / osgi / useradmin / AbstractUserDirectory.java
1 package org.argeo.osgi.useradmin;
2
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;
13
14 import java.io.File;
15 import java.net.URI;
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;
24
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;
35
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;
46
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);
51
52 private final Hashtable<String, Object> properties;
53 private final String baseDn;
54 private final String userObjectClass;
55 private final String groupObjectClass;
56
57 private final boolean readOnly;
58 private final URI uri;
59
60 private UserAdmin externalRoles;
61 private List<String> indexedUserProperties = Arrays.asList(new String[] {
62 LdifName.uid.name(), LdifName.mail.name(), LdifName.cn.name() });
63
64 private String memberAttributeId = "member";
65 private List<String> credentialAttributeIds = Arrays
66 .asList(new String[] { LdifName.userPassword.name() });
67
68 private TransactionManager transactionManager;
69 // private TransactionSynchronizationRegistry transactionRegistry;
70 // private Xid editingTransactionXid = null;
71 private WcXaResource xaResource = new WcXaResource(this);
72
73 // POSIX
74 private String homeDirectoryBase = "/home";
75
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));
81 }
82
83 String uriStr = UserAdminConf.uri.getValue(properties);
84 if (uriStr == null)
85 uri = null;
86 else
87 try {
88 uri = new URI(uriStr);
89 } catch (URISyntaxException e) {
90 throw new UserDirectoryException("Badly formatted URI "
91 + uriStr, e);
92 }
93
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));
100 } else
101 readOnly = new Boolean(readOnlyStr);
102
103 userObjectClass = UserAdminConf.userObjectClass.getValue(properties);
104 groupObjectClass = UserAdminConf.groupObjectClass.getValue(properties);
105 }
106
107 /** Returns the groups this user is a direct member of. */
108 protected abstract List<LdapName> getDirectGroups(LdapName dn);
109
110 protected abstract Boolean daoHasRole(LdapName dn);
111
112 protected abstract DirectoryUser daoGetRole(LdapName key);
113
114 protected abstract List<DirectoryUser> doGetRoles(Filter f);
115
116 public void init() {
117
118 }
119
120 public void destroy() {
121
122 }
123
124 boolean isEditing() {
125 // if (editingTransactionXid == null)
126 // return false;
127 // return workingCopy.get() != null;
128 return xaResource.wc() != null;
129 }
130
131 protected UserDirectoryWorkingCopy getWorkingCopy() {
132 // UserDirectoryWorkingCopy wc = workingCopy.get();
133 UserDirectoryWorkingCopy wc = xaResource.wc();
134 if (wc == null)
135 return null;
136 // if (wc.getXid() == null) {
137 // workingCopy.set(null);
138 // return null;
139 // }
140 return wc;
141 }
142
143 void checkEdit() {
144 Transaction transaction;
145 try {
146 transaction = transactionManager.getTransaction();
147 } catch (SystemException e) {
148 throw new UserDirectoryException("Cannot get transaction", e);
149 }
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);
155 try {
156 transaction.enlistResource(xaResource);
157 // editingTransactionXid = wc.getXid();
158 // workingCopy.set(wc);
159 } catch (Exception e) {
160 throw new UserDirectoryException("Cannot enlist " + xaResource,
161 e);
162 }
163 } else {
164 // UserDirectoryWorkingCopy wc = xaResource.wc();
165 // if (wc == null)
166 // throw new UserDirectoryException("Transaction "
167 // + editingTransactionXid + " already editing");
168 // else if
169 // (!editingTransactionXid.equals(workingCopy.get().getXid()))
170 // throw new UserDirectoryException("Working copy Xid "
171 // + workingCopy.get().getXid() + " inconsistent with"
172 // + editingTransactionXid);
173 }
174 }
175
176 List<Role> getAllRoles(DirectoryUser user) {
177 List<Role> allRoles = new ArrayList<Role>();
178 if (user != null) {
179 collectRoles(user, allRoles);
180 allRoles.add(user);
181 } else
182 collectAnonymousRoles(allRoles);
183 return allRoles;
184 }
185
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);
190 allRoles.add(group);
191 collectRoles(group, allRoles);
192 }
193 }
194
195 private void collectAnonymousRoles(List<Role> allRoles) {
196 // TODO gather anonymous roles
197 }
198
199 // USER ADMIN
200 @Override
201 public Role getRole(String name) {
202 return doGetRole(toDn(name));
203 }
204
205 protected DirectoryUser doGetRole(LdapName dn) {
206 UserDirectoryWorkingCopy wc = getWorkingCopy();
207 DirectoryUser user = daoGetRole(dn);
208 if (wc != null) {
209 if (user == null && wc.getNewUsers().containsKey(dn))
210 user = wc.getNewUsers().get(dn);
211 else if (wc.getDeletedUsers().containsKey(dn))
212 user = null;
213 }
214 return user;
215 }
216
217 @SuppressWarnings("unchecked")
218 @Override
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);
223 if (wc != null) {
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))
228 it.remove();
229 }
230 for (DirectoryUser user : wc.getNewUsers().values()) {
231 if (f == null || f.match(user.getProperties()))
232 res.add(user);
233 }
234 // no need to check modified users,
235 // since doGetRoles was already based on the modified attributes
236 }
237 return res.toArray(new Role[res.size()]);
238 }
239
240 @Override
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());
245 if (key != null) {
246 doGetUser(key, value, collectedUsers);
247 } else {
248 // try dn
249 DirectoryUser user = null;
250 try {
251 user = (DirectoryUser) getRole(value);
252 if (user != null)
253 collectedUsers.add(user);
254 } catch (Exception e) {
255 // silent
256 }
257 // try all indexes
258 for (String attr : getIndexedUserProperties())
259 doGetUser(attr, value, collectedUsers);
260 }
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);
266 return null;
267 }
268
269 protected void doGetUser(String key, String value,
270 List<DirectoryUser> collectedUsers) {
271 try {
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
278 + "=" + value, e);
279 }
280 }
281
282 @Override
283 public Authorization getAuthorization(User user) {
284 return new LdifAuthorization((DirectoryUser) user,
285 getAllRoles((DirectoryUser) user));
286 }
287
288 @Override
289 public Role createRole(String name, int type) {
290 checkEdit();
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);
304 } else {
305 wc.getModifiedUsers().put(dn, attrs);
306 DirectoryUser newRole = newRole(dn, type, attrs);
307 wc.getNewUsers().put(dn, newRole);
308 }
309 return getRole(name);
310 }
311
312 protected DirectoryUser newRole(LdapName dn, int type, Attributes attrs) {
313 LdifUser newRole;
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());
322
323 String username;
324 try {
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);
330 }
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());
343 }
344 objClass.add(top);
345 attrs.put(objClass);
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));
352 objClass.add(top);
353 attrs.put(objClass);
354 newRole = new LdifGroup(this, dn, attrs);
355 } else
356 throw new UserDirectoryException("Unsupported type " + type);
357 return newRole;
358 }
359
360 @Override
361 public boolean removeRole(String name) {
362 checkEdit();
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;
372 }
373 for (LdapName groupDn : getDirectGroups(dn)) {
374 DirectoryUser group = doGetRole(groupDn);
375 group.getAttributes().get(getMemberAttributeId())
376 .remove(dn.toString());
377 }
378 return actuallyDeleted;
379 }
380
381 // POSIX
382 /** Generate path for a new user home */
383 protected String generateHomeDirectory(String username) {
384 String base = homeDirectoryBase;
385 int atIndex = username.indexOf('@');
386 if (atIndex > 0) {
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);
393 } else {
394 return base + '/' + firstCharsToPath(username, 2) + '/' + username;
395 }
396 }
397
398 protected long max(String attr) {
399 long max;
400 try {
401 List<DirectoryUser> users = doGetRoles(FrameworkUtil
402 .createFilter("(" + attr + "=*)"));
403 max = 1000;
404 for (DirectoryUser user : users) {
405 long uid = Long.parseLong(user.getAttributes().get(attr).get()
406 .toString());
407 if (uid > max)
408 max = uid;
409 }
410 } catch (Exception e) {
411 throw new UserDirectoryException("Cannot get max of " + attr, e);
412 }
413 return max;
414 }
415
416 /**
417 * Creates depth from a string (typically a username) by adding levels based
418 * on its first characters: "aBcD",2 => a/aB
419 */
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));
428 path.append(curr);
429 if (i < nbrOfChars - 1)
430 path.append('/');
431 }
432 return path.toString();
433 }
434
435 // TRANSACTION
436 protected void prepare(UserDirectoryWorkingCopy wc) {
437
438 }
439
440 protected void commit(UserDirectoryWorkingCopy wc) {
441
442 }
443
444 protected void rollback(UserDirectoryWorkingCopy wc) {
445
446 }
447
448 // void clearEditingTransactionXid() {
449 // editingTransactionXid = null;
450 // }
451
452 // UTILITIES
453 protected LdapName toDn(String name) {
454 try {
455 return new LdapName(name);
456 } catch (InvalidNameException e) {
457 throw new UserDirectoryException("Badly formatted name", e);
458 }
459 }
460
461 // GETTERS
462
463 String getMemberAttributeId() {
464 return memberAttributeId;
465 }
466
467 List<String> getCredentialAttributeIds() {
468 return credentialAttributeIds;
469 }
470
471 protected URI getUri() {
472 return uri;
473 }
474
475 protected List<String> getIndexedUserProperties() {
476 return indexedUserProperties;
477 }
478
479 protected void setIndexedUserProperties(List<String> indexedUserProperties) {
480 this.indexedUserProperties = indexedUserProperties;
481 }
482
483 private static boolean readOnlyDefault(URI uri) {
484 if (uri == null)
485 return true;
486 if (uri.getScheme().equals("file")) {
487 File file = new File(uri);
488 if (file.exists())
489 return !file.canWrite();
490 else
491 return !file.getParentFile().canWrite();
492 }
493 return true;
494 }
495
496 public boolean isReadOnly() {
497 return readOnly;
498 }
499
500 UserAdmin getExternalRoles() {
501 return externalRoles;
502 }
503
504 public String getBaseDn() {
505 return baseDn;
506 }
507
508 /** dn can be null, in that case a default should be returned. */
509 protected String getUserObjectClass() {
510 return userObjectClass;
511 }
512
513 protected String newUserObjectClass(LdapName dn) {
514 if (dn != null
515 && dn.getRdn(dn.size() - 1).toAttributes().get(uid.name()) != null)
516 return posixAccount.name();
517 else
518 return getUserObjectClass();
519 }
520
521 protected String getGroupObjectClass() {
522 return groupObjectClass;
523 }
524
525 public Dictionary<String, ?> getProperties() {
526 return properties;
527 }
528
529 public void setExternalRoles(UserAdmin externalRoles) {
530 this.externalRoles = externalRoles;
531 }
532
533 public void setTransactionManager(TransactionManager transactionManager) {
534 this.transactionManager = transactionManager;
535 }
536
537 public WcXaResource getXaResource() {
538 return xaResource;
539 }
540
541 }