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