]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.security.core/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java
Node registration
[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.inetOrgPerson;
4 import static org.argeo.osgi.useradmin.LdifName.objectClass;
5 import static org.argeo.osgi.useradmin.LdifName.organizationalPerson;
6 import static org.argeo.osgi.useradmin.LdifName.person;
7 import static org.argeo.osgi.useradmin.LdifName.top;
8
9 import java.io.File;
10 import java.net.URI;
11 import java.net.URISyntaxException;
12 import java.util.ArrayList;
13 import java.util.Arrays;
14 import java.util.Dictionary;
15 import java.util.HashMap;
16 import java.util.Iterator;
17 import java.util.List;
18 import java.util.Map;
19
20 import javax.naming.InvalidNameException;
21 import javax.naming.directory.Attributes;
22 import javax.naming.directory.BasicAttribute;
23 import javax.naming.directory.BasicAttributes;
24 import javax.naming.ldap.LdapName;
25 import javax.naming.ldap.Rdn;
26 import javax.transaction.SystemException;
27 import javax.transaction.Transaction;
28 import javax.transaction.TransactionManager;
29 import javax.transaction.xa.XAException;
30 import javax.transaction.xa.XAResource;
31 import javax.transaction.xa.Xid;
32
33 import org.apache.commons.logging.Log;
34 import org.apache.commons.logging.LogFactory;
35 import org.osgi.framework.Filter;
36 import org.osgi.framework.FrameworkUtil;
37 import org.osgi.framework.InvalidSyntaxException;
38 import org.osgi.service.useradmin.Authorization;
39 import org.osgi.service.useradmin.Group;
40 import org.osgi.service.useradmin.Role;
41 import org.osgi.service.useradmin.User;
42 import org.osgi.service.useradmin.UserAdmin;
43
44 public abstract class AbstractUserDirectory implements UserAdmin {
45 private final static Log log = LogFactory
46 .getLog(AbstractUserDirectory.class);
47
48 private Dictionary<String, ?> properties;
49 private String baseDn = "dc=example,dc=com";
50 private String userObjectClass;
51 private String groupObjectClass;
52
53 private boolean isReadOnly;
54 private URI uri;
55
56 private UserAdmin externalRoles;
57 private List<String> indexedUserProperties = Arrays.asList(new String[] {
58 LdifName.uid.name(), LdifName.mail.name(), LdifName.cn.name() });
59
60 private String memberAttributeId = "member";
61 private List<String> credentialAttributeIds = Arrays
62 .asList(new String[] { LdifName.userpassword.name() });
63
64 // private TransactionSynchronizationRegistry syncRegistry;
65 // private Object editingTransactionKey = null;
66
67 private TransactionManager transactionManager;
68 private ThreadLocal<WorkingCopy> workingCopy = new ThreadLocal<AbstractUserDirectory.WorkingCopy>();
69 private Xid editingTransactionXid = null;
70
71 public AbstractUserDirectory(Dictionary<String, ?> properties) {
72 // TODO make a copy?
73 this.properties = properties;
74
75 String uriStr = LdapProperties.uri.getValue(properties);
76 if (uriStr == null)
77 uri = null;
78 else
79 try {
80 uri = new URI(uriStr);
81 } catch (URISyntaxException e) {
82 throw new UserDirectoryException("Badly formatted URI", e);
83 }
84
85 baseDn = LdapProperties.baseDn.getValue(properties).toString();
86 String isReadOnly = LdapProperties.readOnly.getValue(properties);
87 if (isReadOnly == null)
88 this.isReadOnly = readOnlyDefault(uri);
89 else
90 this.isReadOnly = new Boolean(isReadOnly);
91
92 this.userObjectClass = LdapProperties.userObjectClass
93 .getValue(properties);
94 this.groupObjectClass = LdapProperties.groupObjectClass
95 .getValue(properties);
96 }
97
98 // public AbstractUserDirectory(URI uri, boolean isReadOnly) {
99 // this.uri = uri;
100 // this.isReadOnly = isReadOnly;
101 // }
102
103 /** Returns the {@link Group}s this user is a direct member of. */
104 protected abstract List<? extends DirectoryGroup> getDirectGroups(User user);
105
106 protected abstract Boolean daoHasRole(LdapName dn);
107
108 protected abstract DirectoryUser daoGetRole(LdapName key);
109
110 protected abstract List<DirectoryUser> doGetRoles(Filter f);
111
112 protected abstract void doGetUser(String key, String value,
113 List<DirectoryUser> collectedUsers);
114
115 public void init() {
116
117 }
118
119 public void destroy() {
120
121 }
122
123 boolean isEditing() {
124 if (editingTransactionXid == null)
125 return false;
126 return workingCopy.get() != null;
127 // Object currentTrKey = syncRegistry.getTransactionKey();
128 // if (currentTrKey == null)
129 // return false;
130 // return editingTransactionKey.equals(currentTrKey);
131 }
132
133 protected WorkingCopy getWorkingCopy() {
134 WorkingCopy wc = workingCopy.get();
135 if (wc == null)
136 return null;
137 if (wc.xid == null) {
138 workingCopy.set(null);
139 return null;
140 }
141 return wc;
142 }
143
144 void checkEdit() {
145 Transaction transaction;
146 try {
147 transaction = transactionManager.getTransaction();
148 } catch (SystemException e) {
149 throw new UserDirectoryException("Cannot get transaction", e);
150 }
151 if (transaction == null)
152 throw new UserDirectoryException(
153 "A transaction needs to be active in order to edit");
154 if (editingTransactionXid == null) {
155 WorkingCopy wc = new WorkingCopy();
156 try {
157 transaction.enlistResource(wc);
158 editingTransactionXid = wc.getXid();
159 workingCopy.set(wc);
160 } catch (Exception e) {
161 throw new UserDirectoryException("Cannot enlist " + wc, e);
162 }
163 } else {
164 if (workingCopy.get() == null)
165 throw new UserDirectoryException("Transaction "
166 + editingTransactionXid + " already editing");
167 else if (!editingTransactionXid.equals(workingCopy.get().getXid()))
168 throw new UserDirectoryException("Working copy Xid "
169 + workingCopy.get().getXid() + " inconsistent with"
170 + editingTransactionXid);
171 }
172 }
173
174 List<Role> getAllRoles(User user) {
175 List<Role> allRoles = new ArrayList<Role>();
176 if (user != null) {
177 collectRoles(user, allRoles);
178 allRoles.add(user);
179 } else
180 collectAnonymousRoles(allRoles);
181 return allRoles;
182 }
183
184 private void collectRoles(User user, List<Role> allRoles) {
185 for (Group group : getDirectGroups(user)) {
186 // TODO check for loops
187 allRoles.add(group);
188 collectRoles(group, allRoles);
189 }
190 }
191
192 private void collectAnonymousRoles(List<Role> allRoles) {
193 // TODO gather anonymous roles
194 }
195
196 // USER ADMIN
197 @Override
198 public Role getRole(String name) {
199 LdapName key = toDn(name);
200 WorkingCopy wc = getWorkingCopy();
201 DirectoryUser user = daoGetRole(key);
202 if (wc != null) {
203 if (user == null && wc.getNewUsers().containsKey(key))
204 user = wc.getNewUsers().get(key);
205 else if (wc.getDeletedUsers().containsKey(key))
206 user = null;
207 }
208 return user;
209 }
210
211 @SuppressWarnings("unchecked")
212 @Override
213 public Role[] getRoles(String filter) throws InvalidSyntaxException {
214 WorkingCopy wc = getWorkingCopy();
215 Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null;
216 List<DirectoryUser> res = doGetRoles(f);
217 if (wc != null) {
218 for (Iterator<DirectoryUser> it = res.iterator(); it.hasNext();) {
219 DirectoryUser user = it.next();
220 LdapName dn = user.getDn();
221 if (wc.getDeletedUsers().containsKey(dn))
222 it.remove();
223 }
224 for (DirectoryUser user : wc.getNewUsers().values()) {
225 if (f == null || f.match(user.getProperties()))
226 res.add(user);
227 }
228 // no need to check modified users,
229 // since doGetRoles was already based on the modified attributes
230 }
231 return res.toArray(new Role[res.size()]);
232 }
233
234 @Override
235 public User getUser(String key, String value) {
236 // TODO check value null or empty
237 List<DirectoryUser> collectedUsers = new ArrayList<DirectoryUser>(
238 getIndexedUserProperties().size());
239 if (key != null) {
240 doGetUser(key, value, collectedUsers);
241 } else {
242 // try dn
243 DirectoryUser user = null;
244 try {
245 user = (DirectoryUser) getRole(value);
246 if (user != null)
247 collectedUsers.add(user);
248 } catch (Exception e) {
249 // silent
250 }
251 // try all indexes
252 for (String attr : getIndexedUserProperties())
253 doGetUser(attr, value, collectedUsers);
254 }
255 if (collectedUsers.size() == 1)
256 return collectedUsers.get(0);
257 return null;
258 }
259
260 @Override
261 public Authorization getAuthorization(User user) {
262 return new LdifAuthorization((DirectoryUser) user,
263 getAllRoles((DirectoryUser) user));
264 }
265
266 @Override
267 public Role createRole(String name, int type) {
268 checkEdit();
269 WorkingCopy wc = getWorkingCopy();
270 LdapName dn = toDn(name);
271 if ((daoHasRole(dn) && !wc.getDeletedUsers().containsKey(dn))
272 || wc.getNewUsers().containsKey(dn))
273 throw new UserDirectoryException("Already a role " + name);
274 BasicAttributes attrs = new BasicAttributes();
275 attrs.put("dn", dn.toString());
276 Rdn nameRdn = dn.getRdn(dn.size() - 1);
277 // TODO deal with multiple attr RDN
278 attrs.put(nameRdn.getType(), nameRdn.getValue());
279 if (wc.getDeletedUsers().containsKey(dn)) {
280 wc.getDeletedUsers().remove(dn);
281 wc.getModifiedUsers().put(dn, attrs);
282 } else {
283 wc.getModifiedUsers().put(dn, attrs);
284 DirectoryUser newRole = newRole(dn, type, attrs);
285 wc.getNewUsers().put(dn, newRole);
286 }
287 return getRole(name);
288 }
289
290 protected DirectoryUser newRole(LdapName dn, int type, Attributes attrs) {
291 LdifUser newRole;
292 BasicAttribute objClass = new BasicAttribute(objectClass.name());
293 if (type == Role.USER) {
294 String userObjClass = getUserObjectClass();
295 objClass.add(userObjClass);
296 if (inetOrgPerson.name().equals(userObjClass)) {
297 objClass.add(organizationalPerson.name());
298 objClass.add(person.name());
299 } else if (organizationalPerson.name().equals(userObjClass)) {
300 objClass.add(person.name());
301 }
302 objClass.add(top);
303 attrs.put(objClass);
304 newRole = new LdifUser(this, dn, attrs);
305 } else if (type == Role.GROUP) {
306 objClass.add(getGroupObjectClass());
307 objClass.add(top);
308 attrs.put(objClass);
309 newRole = new LdifGroup(this, dn, attrs);
310 } else
311 throw new UserDirectoryException("Unsupported type " + type);
312 return newRole;
313 }
314
315 @Override
316 public boolean removeRole(String name) {
317 checkEdit();
318 WorkingCopy wc = getWorkingCopy();
319 LdapName dn = toDn(name);
320 if (!daoHasRole(dn) && !wc.getNewUsers().containsKey(dn))
321 return false;
322 DirectoryUser user = (DirectoryUser) getRole(name);
323 wc.getDeletedUsers().put(dn, user);
324 // FIXME clarify directgroups
325 for (DirectoryGroup group : getDirectGroups(user)) {
326 group.getAttributes().get(getMemberAttributeId())
327 .remove(dn.toString());
328 }
329 return true;
330 }
331
332 // TRANSACTION
333 protected void prepare(WorkingCopy wc) {
334
335 }
336
337 protected void commit(WorkingCopy wc) {
338
339 }
340
341 protected void rollback(WorkingCopy wc) {
342
343 }
344
345 // UTILITIES
346 protected LdapName toDn(String name) {
347 try {
348 return new LdapName(name);
349 } catch (InvalidNameException e) {
350 throw new UserDirectoryException("Badly formatted name", e);
351 }
352 }
353
354 // GETTERS
355
356 String getMemberAttributeId() {
357 return memberAttributeId;
358 }
359
360 List<String> getCredentialAttributeIds() {
361 return credentialAttributeIds;
362 }
363
364 protected URI getUri() {
365 return uri;
366 }
367
368 protected void setUri(URI uri) {
369 this.uri = uri;
370 }
371
372 protected List<String> getIndexedUserProperties() {
373 return indexedUserProperties;
374 }
375
376 protected void setIndexedUserProperties(List<String> indexedUserProperties) {
377 this.indexedUserProperties = indexedUserProperties;
378 }
379
380 protected void setReadOnly(boolean isReadOnly) {
381 this.isReadOnly = isReadOnly;
382 }
383
384 private static boolean readOnlyDefault(URI uri) {
385 if (uri == null)
386 return true;
387 if (uri.getScheme().equals("file")) {
388 File file = new File(uri);
389 return !file.canWrite();
390 }
391 return true;
392 }
393
394 public boolean isReadOnly() {
395 return isReadOnly;
396 }
397
398 UserAdmin getExternalRoles() {
399 return externalRoles;
400 }
401
402 public String getBaseDn() {
403 return baseDn;
404 }
405
406 protected String getUserObjectClass() {
407 return userObjectClass;
408 }
409
410 protected String getGroupObjectClass() {
411 return groupObjectClass;
412 }
413
414 protected Dictionary<String, ?> getProperties() {
415 return properties;
416 }
417
418 public void setExternalRoles(UserAdmin externalRoles) {
419 this.externalRoles = externalRoles;
420 }
421
422 public void setTransactionManager(TransactionManager transactionManager) {
423 this.transactionManager = transactionManager;
424 }
425
426 //
427 // XA RESOURCE
428 //
429 protected class WorkingCopy implements XAResource {
430 private Xid xid;
431 private int transactionTimeout = 0;
432
433 private Map<LdapName, DirectoryUser> newUsers = new HashMap<LdapName, DirectoryUser>();
434 private Map<LdapName, Attributes> modifiedUsers = new HashMap<LdapName, Attributes>();
435 private Map<LdapName, DirectoryUser> deletedUsers = new HashMap<LdapName, DirectoryUser>();
436
437 @Override
438 public void start(Xid xid, int flags) throws XAException {
439 if (editingTransactionXid != null)
440 throw new UserDirectoryException("Transaction "
441 + editingTransactionXid + " already editing");
442 this.xid = xid;
443 }
444
445 @Override
446 public void end(Xid xid, int flags) throws XAException {
447 checkXid(xid);
448
449 // clean collections
450 newUsers.clear();
451 newUsers = null;
452 modifiedUsers.clear();
453 modifiedUsers = null;
454 deletedUsers.clear();
455 deletedUsers = null;
456
457 // clean IDs
458 this.xid = null;
459 editingTransactionXid = null;
460 }
461
462 @Override
463 public int prepare(Xid xid) throws XAException {
464 checkXid(xid);
465 if (noModifications())
466 return XA_RDONLY;
467 try {
468 AbstractUserDirectory.this.prepare(this);
469 } catch (Exception e) {
470 log.error("Cannot prepare " + xid, e);
471 throw new XAException(XAException.XA_RBOTHER);
472 }
473 return XA_OK;
474 }
475
476 @Override
477 public void commit(Xid xid, boolean onePhase) throws XAException {
478 checkXid(xid);
479 if (noModifications())
480 return;
481 try {
482 if (onePhase)
483 AbstractUserDirectory.this.prepare(this);
484 AbstractUserDirectory.this.commit(this);
485 } catch (Exception e) {
486 log.error("Cannot commit " + xid, e);
487 throw new XAException(XAException.XA_RBOTHER);
488 }
489 }
490
491 @Override
492 public void rollback(Xid xid) throws XAException {
493 checkXid(xid);
494 try {
495 AbstractUserDirectory.this.rollback(this);
496 } catch (Exception e) {
497 log.error("Cannot rollback " + xid, e);
498 throw new XAException(XAException.XA_HEURMIX);
499 }
500 }
501
502 @Override
503 public void forget(Xid xid) throws XAException {
504 throw new UnsupportedOperationException();
505 }
506
507 @Override
508 public boolean isSameRM(XAResource xares) throws XAException {
509 return xares == this;
510 }
511
512 @Override
513 public Xid[] recover(int flag) throws XAException {
514 throw new UnsupportedOperationException();
515 }
516
517 @Override
518 public int getTransactionTimeout() throws XAException {
519 return transactionTimeout;
520 }
521
522 @Override
523 public boolean setTransactionTimeout(int seconds) throws XAException {
524 transactionTimeout = seconds;
525 return true;
526 }
527
528 private Xid getXid() {
529 return xid;
530 }
531
532 private void checkXid(Xid xid) throws XAException {
533 if (this.xid == null)
534 throw new XAException(XAException.XAER_OUTSIDE);
535 if (!this.xid.equals(xid))
536 throw new XAException(XAException.XAER_NOTA);
537 }
538
539 @Override
540 protected void finalize() throws Throwable {
541 if (editingTransactionXid != null)
542 log.warn("Editing transaction still referenced but no working copy "
543 + editingTransactionXid);
544 editingTransactionXid = null;
545 }
546
547 public boolean noModifications() {
548 return newUsers.size() == 0 && modifiedUsers.size() == 0
549 && deletedUsers.size() == 0;
550 }
551
552 public Attributes getAttributes(LdapName dn) {
553 if (modifiedUsers.containsKey(dn))
554 return modifiedUsers.get(dn);
555 return null;
556 }
557
558 public void startEditing(DirectoryUser user) {
559 LdapName dn = user.getDn();
560 if (modifiedUsers.containsKey(dn))
561 throw new UserDirectoryException("Already editing " + dn);
562 modifiedUsers.put(dn, (Attributes) user.getAttributes().clone());
563 }
564
565 public Map<LdapName, DirectoryUser> getNewUsers() {
566 return newUsers;
567 }
568
569 public Map<LdapName, DirectoryUser> getDeletedUsers() {
570 return deletedUsers;
571 }
572
573 public Map<LdapName, Attributes> getModifiedUsers() {
574 return modifiedUsers;
575 }
576
577 }
578 }