]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.enterprise/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java
Improve user directory
[lgpl/argeo-commons.git] / org.argeo.enterprise / src / org / argeo / osgi / useradmin / AbstractUserDirectory.java
1 package org.argeo.osgi.useradmin;
2
3 import static org.argeo.naming.LdapAttrs.objectClass;
4 import static org.argeo.naming.LdapObjs.extensibleObject;
5 import static org.argeo.naming.LdapObjs.inetOrgPerson;
6 import static org.argeo.naming.LdapObjs.organizationalPerson;
7 import static org.argeo.naming.LdapObjs.person;
8 import static org.argeo.naming.LdapObjs.top;
9
10 import java.io.File;
11 import java.net.URI;
12 import java.net.URISyntaxException;
13 import java.util.ArrayList;
14 import java.util.Arrays;
15 import java.util.Dictionary;
16 import java.util.Enumeration;
17 import java.util.Hashtable;
18 import java.util.Iterator;
19 import java.util.List;
20
21 import javax.naming.InvalidNameException;
22 import javax.naming.NameNotFoundException;
23 import javax.naming.NamingEnumeration;
24 import javax.naming.directory.Attribute;
25 import javax.naming.directory.Attributes;
26 import javax.naming.directory.BasicAttribute;
27 import javax.naming.directory.BasicAttributes;
28 import javax.naming.ldap.LdapName;
29 import javax.naming.ldap.Rdn;
30 import javax.transaction.SystemException;
31 import javax.transaction.Transaction;
32 import javax.transaction.TransactionManager;
33
34 import org.apache.commons.logging.Log;
35 import org.apache.commons.logging.LogFactory;
36 import org.argeo.naming.LdapAttrs;
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 public abstract class AbstractUserDirectory implements UserAdmin, UserDirectory {
47 static final String SHARED_STATE_USERNAME = "javax.security.auth.login.name";
48 static final String SHARED_STATE_PASSWORD = "javax.security.auth.login.password";
49
50 private final static Log log = LogFactory.getLog(AbstractUserDirectory.class);
51
52 private final Hashtable<String, Object> properties;
53 private final LdapName baseDn, userBaseDn, groupBaseDn;
54 private final String userObjectClass, userBase, groupObjectClass, groupBase;
55
56 private final boolean readOnly;
57 private final URI uri;
58
59 private UserAdmin externalRoles;
60 // private List<String> indexedUserProperties = Arrays
61 // .asList(new String[] { LdapAttrs.uid.name(), LdapAttrs.mail.name(),
62 // LdapAttrs.cn.name() });
63
64 private String memberAttributeId = "member";
65 private List<String> credentialAttributeIds = Arrays
66 .asList(new String[] { LdapAttrs.userPassword.name(), LdapAttrs.authPassword.name() });
67
68 // JTA
69 private TransactionManager transactionManager;
70 private WcXaResource xaResource = new WcXaResource(this);
71
72 public AbstractUserDirectory(URI uriArg, Dictionary<String, ?> props) {
73 properties = new Hashtable<String, Object>();
74 for (Enumeration<String> keys = props.keys(); keys.hasMoreElements();) {
75 String key = keys.nextElement();
76 properties.put(key, props.get(key));
77 }
78
79 if (uriArg != null) {
80 uri = uriArg;
81 // uri from properties is ignored
82 } else {
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 " + uriStr, e);
91 }
92 }
93
94 userObjectClass = UserAdminConf.userObjectClass.getValue(properties);
95 userBase = UserAdminConf.userBase.getValue(properties);
96 groupObjectClass = UserAdminConf.groupObjectClass.getValue(properties);
97 groupBase = UserAdminConf.groupBase.getValue(properties);
98 try {
99 baseDn = new LdapName(UserAdminConf.baseDn.getValue(properties));
100 userBaseDn = new LdapName(userBase + "," + baseDn);
101 groupBaseDn = new LdapName(groupBase + "," + baseDn);
102 } catch (InvalidNameException e) {
103 throw new UserDirectoryException("Badly formated base DN " + UserAdminConf.baseDn.getValue(properties), e);
104 }
105 String readOnlyStr = UserAdminConf.readOnly.getValue(properties);
106 if (readOnlyStr == null) {
107 readOnly = readOnlyDefault(uri);
108 properties.put(UserAdminConf.readOnly.name(), Boolean.toString(readOnly));
109 } else
110 readOnly = new Boolean(readOnlyStr);
111 }
112
113 /** Returns the groups this user is a direct member of. */
114 protected abstract List<LdapName> getDirectGroups(LdapName dn);
115
116 protected abstract Boolean daoHasRole(LdapName dn);
117
118 protected abstract DirectoryUser daoGetRole(LdapName key) throws NameNotFoundException;
119
120 protected abstract List<DirectoryUser> doGetRoles(Filter f);
121
122 protected abstract AbstractUserDirectory scope(User user);
123
124 public void init() {
125
126 }
127
128 public void destroy() {
129
130 }
131
132 protected boolean isEditing() {
133 return xaResource.wc() != null;
134 }
135
136 protected UserDirectoryWorkingCopy getWorkingCopy() {
137 UserDirectoryWorkingCopy wc = xaResource.wc();
138 if (wc == null)
139 return null;
140 return wc;
141 }
142
143 protected 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("A transaction needs to be active in order to edit");
152 if (xaResource.wc() == null) {
153 try {
154 transaction.enlistResource(xaResource);
155 } catch (Exception e) {
156 throw new UserDirectoryException("Cannot enlist " + xaResource, e);
157 }
158 } else {
159 }
160 }
161
162 protected List<Role> getAllRoles(DirectoryUser user) {
163 List<Role> allRoles = new ArrayList<Role>();
164 if (user != null) {
165 collectRoles(user, allRoles);
166 allRoles.add(user);
167 } else
168 collectAnonymousRoles(allRoles);
169 return allRoles;
170 }
171
172 private void collectRoles(DirectoryUser user, List<Role> allRoles) {
173 Attributes attrs = user.getAttributes();
174 // TODO centralize attribute name
175 Attribute memberOf = attrs.get(LdapAttrs.memberOf.name());
176 if (memberOf != null) {
177 try {
178 NamingEnumeration<?> values = memberOf.getAll();
179 while (values.hasMore()) {
180 Object value = values.next();
181 LdapName groupDn = new LdapName(value.toString());
182 DirectoryUser group = doGetRole(groupDn);
183 allRoles.add(group);
184 if (log.isDebugEnabled())
185 log.debug("Add memberOf " + groupDn);
186 }
187 } catch (Exception e) {
188 throw new UserDirectoryException("Cannot get memberOf groups for " + user, e);
189 }
190 } else {
191 for (LdapName groupDn : getDirectGroups(user.getDn())) {
192 // TODO check for loops
193 DirectoryUser group = doGetRole(groupDn);
194 allRoles.add(group);
195 if (log.isDebugEnabled())
196 log.debug("Add direct group " + groupDn);
197 collectRoles(group, allRoles);
198 }
199 }
200 }
201
202 private void collectAnonymousRoles(List<Role> allRoles) {
203 // TODO gather anonymous roles
204 }
205
206 // USER ADMIN
207 @Override
208 public Role getRole(String name) {
209 return doGetRole(toDn(name));
210 }
211
212 protected DirectoryUser doGetRole(LdapName dn) {
213 UserDirectoryWorkingCopy wc = getWorkingCopy();
214 DirectoryUser user;
215 try {
216 user = daoGetRole(dn);
217 } catch (NameNotFoundException e) {
218 user = null;
219 }
220 if (wc != null) {
221 if (user == null && wc.getNewUsers().containsKey(dn))
222 user = wc.getNewUsers().get(dn);
223 else if (wc.getDeletedUsers().containsKey(dn))
224 user = null;
225 }
226 return user;
227 }
228
229 @SuppressWarnings("unchecked")
230 @Override
231 public Role[] getRoles(String filter) throws InvalidSyntaxException {
232 UserDirectoryWorkingCopy wc = getWorkingCopy();
233 Filter f = filter != null ? FrameworkUtil.createFilter(filter) : null;
234 List<DirectoryUser> res = doGetRoles(f);
235 if (wc != null) {
236 for (Iterator<DirectoryUser> it = res.iterator(); it.hasNext();) {
237 DirectoryUser user = it.next();
238 LdapName dn = user.getDn();
239 if (wc.getDeletedUsers().containsKey(dn))
240 it.remove();
241 }
242 for (DirectoryUser user : wc.getNewUsers().values()) {
243 if (f == null || f.match(user.getProperties()))
244 res.add(user);
245 }
246 // no need to check modified users,
247 // since doGetRoles was already based on the modified attributes
248 }
249 return res.toArray(new Role[res.size()]);
250 }
251
252 @Override
253 public User getUser(String key, String value) {
254 // TODO check value null or empty
255 List<DirectoryUser> collectedUsers = new ArrayList<DirectoryUser>();
256 if (key != null) {
257 doGetUser(key, value, collectedUsers);
258 } else {
259 throw new UserDirectoryException("Key cannot be null");
260 // // try dn
261 // DirectoryUser user = null;
262 // try {
263 // user = (DirectoryUser) getRole(value);
264 // if (user != null)
265 // collectedUsers.add(user);
266 // } catch (Exception e) {
267 // // silent
268 // }
269 // // try all indexes
270 // for (String attr : getIndexedUserProperties())
271 // doGetUser(attr, value, collectedUsers);
272 }
273 if (collectedUsers.size() == 1)
274 return collectedUsers.get(0);
275 else if (collectedUsers.size() > 1)
276 log.warn(collectedUsers.size() + " users for " + (key != null ? key + "=" : "") + value);
277 return null;
278 }
279
280 protected void doGetUser(String key, String value, List<DirectoryUser> collectedUsers) {
281 try {
282 Filter f = FrameworkUtil.createFilter("(" + key + "=" + value + ")");
283 List<DirectoryUser> users = doGetRoles(f);
284 collectedUsers.addAll(users);
285 } catch (InvalidSyntaxException e) {
286 throw new UserDirectoryException("Cannot get user with " + key + "=" + value, e);
287 }
288 }
289
290 @Override
291 public Authorization getAuthorization(User user) {
292 if (user == null || user instanceof DirectoryUser) {
293 return new LdifAuthorization(user, getAllRoles((DirectoryUser) user));
294 } else {
295 // bind
296 AbstractUserDirectory scopedUserAdmin = scope(user);
297 try {
298 DirectoryUser directoryUser = (DirectoryUser) scopedUserAdmin.getRole(user.getName());
299 if (directoryUser == null)
300 throw new UserDirectoryException("No scoped user found for " + user);
301 LdifAuthorization authorization = new LdifAuthorization(directoryUser,
302 scopedUserAdmin.getAllRoles(directoryUser));
303 return authorization;
304 } finally {
305 scopedUserAdmin.destroy();
306 }
307 }
308 }
309
310 @Override
311 public Role createRole(String name, int type) {
312 checkEdit();
313 UserDirectoryWorkingCopy wc = getWorkingCopy();
314 LdapName dn = toDn(name);
315 if ((daoHasRole(dn) && !wc.getDeletedUsers().containsKey(dn)) || wc.getNewUsers().containsKey(dn))
316 throw new UserDirectoryException("Already a role " + name);
317 BasicAttributes attrs = new BasicAttributes(true);
318 // attrs.put(LdifName.dn.name(), dn.toString());
319 Rdn nameRdn = dn.getRdn(dn.size() - 1);
320 // TODO deal with multiple attr RDN
321 attrs.put(nameRdn.getType(), nameRdn.getValue());
322 if (wc.getDeletedUsers().containsKey(dn)) {
323 wc.getDeletedUsers().remove(dn);
324 wc.getModifiedUsers().put(dn, attrs);
325 return getRole(name);
326 } else {
327 wc.getModifiedUsers().put(dn, attrs);
328 DirectoryUser newRole = newRole(dn, type, attrs);
329 wc.getNewUsers().put(dn, newRole);
330 return newRole;
331 }
332 }
333
334 protected DirectoryUser newRole(LdapName dn, int type, Attributes attrs) {
335 LdifUser newRole;
336 BasicAttribute objClass = new BasicAttribute(objectClass.name());
337 if (type == Role.USER) {
338 String userObjClass = newUserObjectClass(dn);
339 objClass.add(userObjClass);
340 if (inetOrgPerson.name().equals(userObjClass)) {
341 objClass.add(organizationalPerson.name());
342 objClass.add(person.name());
343 } else if (organizationalPerson.name().equals(userObjClass)) {
344 objClass.add(person.name());
345 }
346 objClass.add(top.name());
347 objClass.add(extensibleObject.name());
348 attrs.put(objClass);
349 newRole = new LdifUser(this, dn, attrs);
350 } else if (type == Role.GROUP) {
351 String groupObjClass = getGroupObjectClass();
352 objClass.add(groupObjClass);
353 // objClass.add(LdifName.extensibleObject.name());
354 objClass.add(top.name());
355 attrs.put(objClass);
356 newRole = new LdifGroup(this, dn, attrs);
357 } else
358 throw new UserDirectoryException("Unsupported type " + type);
359 return newRole;
360 }
361
362 @Override
363 public boolean removeRole(String name) {
364 checkEdit();
365 UserDirectoryWorkingCopy wc = getWorkingCopy();
366 LdapName dn = toDn(name);
367 boolean actuallyDeleted;
368 if (daoHasRole(dn) || wc.getNewUsers().containsKey(dn)) {
369 DirectoryUser user = (DirectoryUser) getRole(name);
370 wc.getDeletedUsers().put(dn, user);
371 actuallyDeleted = true;
372 } else {// just removing from groups (e.g. system roles)
373 actuallyDeleted = false;
374 }
375 for (LdapName groupDn : getDirectGroups(dn)) {
376 DirectoryUser group = doGetRole(groupDn);
377 group.getAttributes().get(getMemberAttributeId()).remove(dn.toString());
378 }
379 return actuallyDeleted;
380 }
381
382 // TRANSACTION
383 protected void prepare(UserDirectoryWorkingCopy wc) {
384
385 }
386
387 protected void commit(UserDirectoryWorkingCopy wc) {
388
389 }
390
391 protected void rollback(UserDirectoryWorkingCopy wc) {
392
393 }
394
395 // UTILITIES
396 protected LdapName toDn(String name) {
397 try {
398 return new LdapName(name);
399 } catch (InvalidNameException e) {
400 throw new UserDirectoryException("Badly formatted name", e);
401 }
402 }
403
404 // GETTERS
405 protected String getMemberAttributeId() {
406 return memberAttributeId;
407 }
408
409 protected List<String> getCredentialAttributeIds() {
410 return credentialAttributeIds;
411 }
412
413 protected URI getUri() {
414 return uri;
415 }
416
417 private static boolean readOnlyDefault(URI uri) {
418 if (uri == null)
419 return true;
420 if (uri.getScheme() == null)
421 return false;// assume relative file to be writable
422 if (uri.getScheme().equals("file")) {
423 File file = new File(uri);
424 if (file.exists())
425 return !file.canWrite();
426 else
427 return !file.getParentFile().canWrite();
428 } else if (uri.getScheme().equals("ldap")) {
429 if (uri.getAuthority() != null)// assume writable if authenticated
430 return false;
431 }
432 return true;// read only by default
433 }
434
435 public boolean isReadOnly() {
436 return readOnly;
437 }
438
439 protected UserAdmin getExternalRoles() {
440 return externalRoles;
441 }
442
443 protected int roleType(LdapName dn) {
444 if (dn.startsWith(groupBaseDn))
445 return Role.GROUP;
446 else if (dn.startsWith(userBaseDn))
447 return Role.USER;
448 else
449 return Role.GROUP;
450 }
451
452 /** dn can be null, in that case a default should be returned. */
453 public String getUserObjectClass() {
454 return userObjectClass;
455 }
456
457 public String getUserBase() {
458 return userBase;
459 }
460
461 protected String newUserObjectClass(LdapName dn) {
462 return getUserObjectClass();
463 }
464
465 public String getGroupObjectClass() {
466 return groupObjectClass;
467 }
468
469 public String getGroupBase() {
470 return groupBase;
471 }
472
473 public LdapName getBaseDn() {
474 return (LdapName) baseDn.clone();
475 }
476
477 public Dictionary<String, Object> getProperties() {
478 return properties;
479 }
480
481 public Dictionary<String, Object> cloneProperties() {
482 return new Hashtable<>(properties);
483 }
484
485 public void setExternalRoles(UserAdmin externalRoles) {
486 this.externalRoles = externalRoles;
487 }
488
489 public void setTransactionManager(TransactionManager transactionManager) {
490 this.transactionManager = transactionManager;
491 }
492
493 public WcXaResource getXaResource() {
494 return xaResource;
495 }
496
497 }