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