View Javadoc

1   /*
2    * NISAccountMapper.java
3    *
4    * Created on May 25, 2004, 2:25 PM
5    */
6   
7   package gov.bnl.gums.account;
8   
9   import gov.bnl.gums.GUMS;
10  import gov.bnl.gums.SiteUser;
11  import gov.bnl.gums.configuration.Configuration;
12  
13  import java.util.*;
14  
15  import javax.naming.*;
16  import javax.naming.directory.*;
17  import javax.naming.ldap.InitialLdapContext;
18  import javax.persistence.Entity;
19  import javax.persistence.Transient;
20  
21  import org.apache.log4j.Logger;
22  
23  
24  /** 
25   * Maps a user to a local account based on the CN of the certificate and the
26   * gecos field in the NIS/YP database. The mapping can't be perfect, but contains
27   * a series of heuristics that solve up to 90% of the cases, depending on how
28   * the NIS database itself is kept.
29   * <p>
30   * It's suggested not to use this policy by itself, but to have it part of a 
31   * CompositeAccountMapper in which a ManualAccountMapper comes first. This allows
32   * to override those user mapping that are not satisfying.
33   *
34   * @author Jay Packard
35   */
36  @Entity
37  public class LdapAccountMapper extends AccountMapper {
38  	static private Logger log = Logger.getLogger(LdapAccountMapper.class);
39  	static private List<DirContext> contexts = Collections.synchronizedList(new LinkedList<DirContext>());// *** LDAP connection pool management
40      static private Logger gumsAdminLog = Logger.getLogger(GUMS.gumsAdminLogName);
41  
42  	protected boolean skipReleaseContext = false;
43  	protected String peopleContext = null;
44  	protected String groupContext = null; 
45  	protected String peopleObject = "ou=People";
46  	protected String groupObject = "ou=Group";
47  
48  	// persistent variables
49  	protected String jndiLdapUrl;
50  	protected String dnField = "description";
51  	protected String accountField = "uid";
52  	protected String memberUidField = "memberUid";
53  	protected String gidNumberField = "gidNumber";
54  	protected String groupCnField = "cn";
55  	protected String peopleTree;
56  	protected String groupTree;
57  	protected boolean synchGroups;
58  	
59  	public LdapAccountMapper() {
60  		super();
61  	}
62  
63  	public LdapAccountMapper(Configuration configuration, String name) {
64  		super(configuration, name);
65  	}
66  
67  	/** 
68       * Adds the account to the given secondary group.
69       * 
70       * @param account the account to add to the secondary group (i.e. "carcassi")
71       * @param groupname the secondary group name (i.e. "usatlas")
72       */
73      public void addToSecondaryGroup(String account, String groupname) {
74          DirContext context = null;
75          try {
76              context = retrieveGroupContext();
77              SearchControls ctrls = new SearchControls();
78              ctrls.setSearchScope(SearchControls.SUBTREE_SCOPE);
79              NamingEnumeration<SearchResult> result;
80              result = context.search(groupCnField+"="+groupname+","+groupObject, "("+memberUidField+"={0})", new Object[] {account}, ctrls);
81              if (result.hasMore()) return;
82              ModificationItem[] mods = new ModificationItem[1];
83              mods[0] = new ModificationItem(DirContext.ADD_ATTRIBUTE, new BasicAttribute(memberUidField, account));
84              context.modifyAttributes(groupCnField+"="+groupname+","+groupObject, mods);
85              log.trace("Added secondary group to user - user '" + account + "' to group '" + groupname + "'");
86          } catch (Exception e) {
87              log.info("Couldn't add user to secondary group - user '" + account + "' to group '" + groupname + "'", e);
88              throw new RuntimeException("Couldn't add user to secondary group - user '" + account + "' to group '" + groupname + "': " + e.getMessage(), e);
89          } finally {
90          	releaseContext(context);
91          }
92  }
93  	
94      /** 
95  	 * Changes the primary gid for the given account.
96  	 * 
97  	 * @param account the account to change the primary group (i.e. "carcassi")
98  	 * @param groupname the primary group name (i.e. "usatlas")
99  	 */
100 	public void changeGroup(String account, String groupname) {
101 		try { 
102 			String gid = findGID(groupname);
103 			if (gid == null) {
104 				log.error("GID for group '" + groupname + "' wasn't found.");
105 				throw new RuntimeException("GID for group '" + groupname + "' wasn't found.");
106 			}
107 			updateGID(account, gid);
108 		}
109 		catch(NamingException e) {
110 			log.error(e.getMessage());
111 			throw new RuntimeException(e.getMessage());
112 		}
113 	}
114 
115 	public String getAccountField() {
116 		return accountField;
117 	}
118 
119 	public String getDnField() {
120 		return dnField;
121 	}
122 
123 	public String getGidNumberField() {
124 		return gidNumberField;
125 	}
126 
127 	public String getGroupCnField() {
128 		return groupCnField;
129 	}
130 
131 	public String getGroupTree() {
132 		return groupTree;
133 	}
134 
135 	public String getJndiLdapUrl() {
136 		return jndiLdapUrl;
137 	}
138 	
139 	public String getMemberUidField() {
140 		return memberUidField;
141 	}
142 	
143 	public String getPeopleTree() {
144 		return peopleTree;
145 	}
146 	
147 	public boolean isSynchGroups() {
148 		return synchGroups;
149 	}
150 	
151 	public SiteUser mapDn(String dn, boolean createIfDoesNotExist) {
152 		Properties jndiProperties = getProperties();
153 		String userDNWithSubject = "subject="+dn;
154 		int nTries = 5;
155 		int i = 0;
156 		for (; i < nTries; i++) {
157 			log.debug("Attempt " + i + " to retrieve map at '" + jndiLdapUrl + "'");
158 			try {
159 				DirContext jndiCtx = new InitialDirContext(jndiProperties);
160 				NamingEnumeration<SearchResult> nisMap = jndiCtx.search(peopleObject, "("+accountField+"=*)", null);
161 				log.debug("Server responded");
162 				while (nisMap.hasMore()) {
163 					SearchResult res = (SearchResult) nisMap.next();
164 					Attributes atts = res.getAttributes();
165 					Attribute dnAtt = atts.get(dnField);
166 					if (dnAtt != null) {
167 						if (dn.equals(dnAtt.get().toString()) || userDNWithSubject.equals(dnAtt.get().toString())) {
168 							String account = (String) atts.get(accountField).get();
169 							log.debug("Found account '" + account + "' for DN '" + dn + "'");
170 							return new SiteUser(account);
171 						}
172 					}
173 				}
174 				jndiCtx.close();
175 			} catch (javax.naming.NamingException ne) {
176 				log.warn("Error searching LDAP at "+jndiLdapUrl, ne);
177 				try {
178 					Thread.sleep(100);
179 				} catch (InterruptedException e) {
180 					log.warn("Interrupted", e);
181 				}
182 			} catch (Exception e) {
183 				log.warn("Error searching LDAP at "+jndiLdapUrl, e);
184 				try {
185 					Thread.sleep(100);
186 				} catch (InterruptedException ie) {
187 					log.warn("Interrupted", e);
188 				}
189 			}
190 		}
191 		return null;       
192 	}
193 	
194 	public DirContext retrieveGroupContext() {
195 		DirContext context;
196 		while (contexts.size() != 0) {
197 			context = (DirContext) contexts.remove(0);
198 			if (isGroupContextValid(context)) {
199 				log.trace("Using LDAP connection from pool " + context);
200 				return context;
201 			}
202 		}
203 		context = createGroupContext();
204 		log.trace("New LDAP connection created " + context);
205 		return context;	
206 	}
207 	
208 	@ConfigFieldAnnotation(label="Account UID Field", example="uid")
209 	public void setAccountField(String accountField) {
210 		this.accountField = accountField;
211 	}
212 
213 	@ConfigFieldAnnotation (label="Certificate DN Field", example="description")
214 	public void setDnField(String dnField) {
215 		this.dnField = dnField;
216 	}
217 
218 	@ConfigFieldAnnotation(label="GID Number Field", example="gidNumber", help="group ID number field in 'People' object")
219 	public void setGidNumberField(String gidNumberField) {
220 		this.gidNumberField = gidNumberField;
221 	}
222 
223 	@ConfigFieldAnnotation(label="Group CN Field", example="cn")
224 	public void setGroupCnField(String groupCnField) {
225 		this.groupCnField = groupCnField;
226 	}
227 
228 	@ConfigFieldAnnotation(label="Group Tree", example="ou=Group,dc=usatlas,dc=bnl,dc=gov", help="relative to context in LDAP URL - optional")
229 	public void setGroupTree(String groupTree) {
230 		if (groupTree != null) {
231 			this.groupTree = groupTree;
232 			if (groupTree.indexOf(',')!=-1) {
233 				this.groupObject = groupTree.substring(0, groupTree.indexOf(','));
234 				this.groupContext = groupTree.substring(groupTree.indexOf(',')+1);   
235 			}
236 			else
237 				this.groupObject = groupTree;
238 		} 
239 	}
240 
241 	@ConfigFieldAnnotation(label="JNDI LDAP URL", example="ldap://localhost/dc=usatlas,dc=bnl,dc=gov") 
242 	public void setJndiLdapUrl(String jndiLdapUrl) {
243 		this.jndiLdapUrl = jndiLdapUrl;
244 	}
245 	
246 	@ConfigFieldAnnotation(label="Member Field", example="memberUid", help="field containing user ID in 'Group' object")
247 	public void setMemberUidField(String memberUidField) {
248 		this.memberUidField = memberUidField;
249 	}
250 	
251 	@ConfigFieldAnnotation(label="People Tree", example="ou=People,dc=usatlas,dc=bnl,dc=gov", help="relative to context in LDAP URL - optional")
252 	public void setPeopleTree(String peopleTree) {
253 		if (peopleTree != null) {
254 			this.peopleTree = peopleTree;
255 			if (peopleTree.indexOf(',')!=-1) {
256 				this.peopleObject = peopleTree.substring(0, peopleTree.indexOf(','));
257 				this.peopleContext = peopleTree.substring(peopleTree.indexOf(',')+1);   
258 			}
259 			else
260 				this.peopleObject = peopleTree;
261 		} 
262 	}
263 
264 	@ConfigFieldAnnotation(label="Update group and email for every access")
265 	public void setSynchGroups(boolean synchGroups) {
266 		this.synchGroups = synchGroups;
267 	}
268 
269 	protected String findGID(String groupname) throws NamingException {
270 		DirContext context = retrieveGroupContext();
271 		try {
272 			NamingEnumeration<SearchResult> result = context.search(groupObject, "("+groupCnField+"={0})", new Object[] {groupname}, null);
273 			String gid = null;
274 			if (result.hasMore()) {
275 				SearchResult item = result.next();
276 				Attributes atts = item.getAttributes();
277 				Attribute gidAtt = atts.get(gidNumberField);
278 				if (gidAtt != null) {
279 					gid = (String) gidAtt.get();
280 				}
281 			}
282 			log.trace("Found gid '" + gid + "' for group '" + groupname + "'");
283 			return gid;
284 		} catch (Exception e) {
285 			log.info("Couldn't retrieve gid for '" + groupname + "'", e);
286 			throw new RuntimeException("Couldn't retrieve gid for '" + groupname + "': " + e.getMessage(), e);
287 		} finally {
288 			releaseContext(context);
289 		}
290 	}
291 
292 	protected void updateGID(String account, String gid) throws NamingException {
293 		DirContext context = retrievePeopleContext();
294 		try {
295 			ModificationItem[] mods = new ModificationItem[1];
296 			mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute(gidNumberField, gid));
297 			context.modifyAttributes(accountField+"="+account+","+peopleObject, mods);
298 			log.trace("Changed primary gid for user '" + account + "' to gid '" + gid + "''");
299 		} catch (Exception e) {
300 			log.warn("Couldn't change gid for user '" + account + "' to gid '" + gid + "''", e);
301 			throw new RuntimeException("Couldn't change gid for user '" + account + "' to gid '" + gid + "''", e);
302 		} finally {
303 			releaseContext(context);
304 		}
305 	}
306 
307 	
308 	protected DirContext createGroupContext() {
309 		Properties properties = (Properties)getProperties();
310 		try {
311 			log.info("Trying to create LDAP connection with properties: " + properties);
312 			return (DirContext)new InitialLdapContext(properties, null);
313 		} catch (NamingException e) {
314 			log.warn("Couldn't create LDAP connection: " + e.getMessage() + " - parameters: " + properties, e);
315 			throw new RuntimeException("Couldn't create LDAP connection: " + e.getMessage());
316 		}
317 	}
318 
319 	protected DirContext createPeopleContext() {
320 		Properties properties = (Properties)getProperties();
321 		try {
322 			log.info("Trying to create LDAP connection with properties: " + properties);
323 			return (DirContext)new InitialLdapContext(properties, null);
324 		} catch (NamingException e) {
325 			log.warn("Couldn't create LDAP connection: " + e.getMessage() + " - parameters: " + getProperties(), e);
326 			throw new RuntimeException("Couldn't create LDAP connection: " + e.getMessage());
327 		}
328 	}
329 
330 	@Transient
331 	protected Properties getProperties() {
332 		Properties properties = new Properties();
333 		properties.setProperty("java.naming.provider.url", jndiLdapUrl+(groupContext!=null?"/"+groupContext:""));
334 		properties.setProperty("java.naming.factory.initial","com.sun.jndi.ldap.LdapCtxFactory");
335 		properties.setProperty(Context.SECURITY_PROTOCOL, "none");
336 		System.out.println(properties);
337 		return properties;
338 	}
339 
340 	protected boolean isGroupContextValid(DirContext context) {
341 		try {
342 			context.search(groupObject, "(" + groupCnField + "=*)", null);
343 			return true;
344 		} catch (Exception e) {
345 			log.trace("Removing stale LDAP connection from pool " + context, e);
346 			gumsAdminLog.warn("LDAP connection test failed, discarding connection from pool: " + e.getMessage());
347 			return false;
348 		}
349 	}
350 
351 	protected boolean isPeopleContextValid(DirContext context) {
352 		try {
353 			context.search(peopleObject, "(" + gidNumberField + "=*)", null);
354 			return true;
355 		} catch (Exception e) {
356 			log.trace("Removing stale LDAP connection from pool " + context, e);
357 			gumsAdminLog.warn("LDAP connection test failed, discarding connection from pool: " + e.getMessage());
358 			return false;
359 		}
360 	}
361 
362 	/** Returns the LDAP DirContext to the pool, so that it can be reused.
363 	 * 
364 	 * @param context the LDAP context to be returned
365 	 */
366 	protected void releaseContext(DirContext context) {
367 		if (skipReleaseContext) {
368 			skipReleaseContext = false;
369 			return;
370 		}
371 		contexts.add(0, context);
372 		log.trace("LDAP connection returned to pool " + context);
373 	}
374 
375 	protected DirContext retrievePeopleContext() {
376 		DirContext context;
377 		while (contexts.size() != 0) {
378 			context = (DirContext) contexts.remove(0);
379 			if (isPeopleContextValid(context)) {
380 				log.trace("Using LDAP connection from pool " + context);
381 				return context;
382 			}
383 		}
384 		context = createPeopleContext();
385 		log.trace("New LDAP connection created " + context);
386 		return context;		
387 	}
388 }