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